Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial attempt to implement transactions #130

Merged
merged 24 commits into from
Sep 16, 2021

Conversation

kevinresol
Copy link
Member

@kevinresol kevinresol commented Apr 19, 2021

My rough idea is to introduce Connection#isolate which will:

  1. on pooled connections, request a connection from the pool and disable auto-release
  2. on single connection, just return the connection itself

Normal queries will just execute as before, i.e. using pooled connections + auto-release when available.

On the other hand, transactions will call isolate under the hood and release the connection back to the pool (if any) after the transaction is finished.

Regarding point 2 in the beginning of this text, since isolate does actually nothing right now, so it is only safe on sync targets. We need some more thoughts on that. Perhaps we need some sort of local lock so that when that single connection is isolated, other requests that lands on the same connection has to wait until the lock is released.

Only Node MySQL for now... my head aches...

benmerckx and others added 6 commits March 18, 2018 22:32
# Conflicts:
#	src/tink/sql/Database.hx
#	src/tink/sql/drivers/node/MySql.hx
#	src/tink/sql/drivers/php/MySQLi.hx
#	src/tink/sql/format/SqlFormatter.hx
#	tests/Run.hx
# Conflicts:
#	src/tink/sql/macros/DatabaseBuilder.hx
@kevinresol
Copy link
Member Author

kevinresol commented Apr 19, 2021

Currently the API is used like so:

db.transaction(trx -> {
  // do business with trx, where $type(trx) == $type(db)
  return Promise.NOISE;
})

One problem is that trx.transaction() is a thing and if used it will fail nastily. I am thinking if it is better to let user define an interface instead of a class, and change Database to extend a generic-built Transaction class, so that one cannot call transaction on a Transaction itself:

interface Db {
  @:table var user:User;
  @:table var post:Post;
}

@:genericBuild(currentDatabaseBuilder())
class Transaction<Db> {} // becomes "class Transaction0 implements Db {...}"

class Database<Db> extends Transaction<Db> {
  public function transaction<T>(run:Transaction<Db>->Promise<TransactionEnd<T>>):Promise<TransactionEnd<T>>;
}

var db = new Database<Db>(name, driver);

@kevinresol kevinresol marked this pull request as draft April 20, 2021 03:17
@kevinresol
Copy link
Member Author

kevinresol commented Apr 20, 2021

I am thinking if it is better to let user define an interface instead of a class, and change Database to extend a generic-built Transaction class

The said change is now in place. There are a few (small) breaking changes.

  1. tink.sql.Database is now generically built and cannot be extended directly.
    // before
    class Db extends Database {
    	@:table var User:User;
    }
    
    // after
    interface Def extends DatabaseDefinition {
    	@:table var User:User;
    }
    typedef Db = Database<Def>;
  2. Because of the above change, extra fields has to be declared in a class extending the generically built database class
    // before
    class Db extends Database {
    	function customFunc():Int;
    }
    
    // after
    interface Def extends DatabaseDefinition {
    	@:table var User:User;
    }
    class MyDb extends Database<Def> {
    	function customFunc():Int;
    }

3. @:tables meta are no longer supported, one should now use @:table meta manually. Reason: since the generically built class cannot access private types specified in @:tables

@kevinresol kevinresol marked this pull request as ready for review April 20, 2021 05:03
@kevinresol kevinresol marked this pull request as draft April 20, 2021 07:30
@kevinresol kevinresol marked this pull request as ready for review April 20, 2021 08:21
@benmerckx
Copy link
Member

One problem is that trx.transaction() is a thing and if used it will fail nastily.

so that one cannot call transaction on a Transaction itself

Don't think you would want to pursue in this effort, but one other way of "going deeper" is using savepoints. They allow you to do "transactions in transactions".

@kevinresol
Copy link
Member Author

kevinresol commented Apr 22, 2021

Currently this is kind of solved by doing so:

  1. User defines a database definition.
    • Before this PR, it is done by, for example, @:tables(User) class Db extends Database {}.
    • After this PR, it is done by @:tables(Users) interface Db extends DatabaseDefinition {}
  2. Initialize the actual database instance:
    • Before this PR, new Db(...)
    • After this PR, new Database<Db>(...). Under the hood, the class is generated as:
    class Database0 implements Db {
       public final User:Table<User>;
       public function transaction(run:Db->Promise<...>);
    }

So, when inside a transaction, user will not be able to call transaction again.

@back2dos
Copy link
Member

Hmm. I can see why you would want to try hiding the transaction method, but what's to stop the user from something like this?

var db = new Database<Db>();
db.transaction(_ -> db.transaction(...));

@kevinresol
Copy link
Member Author

kevinresol commented Apr 22, 2021

The inner transaction will be performed in another isolated connection if there is a pool, or wait until the outer one finishes and releases the connection if there is no pool.

@back2dos
Copy link
Member

Hmm, ok, I think I understand. I would suggest a slightly different approach then:

  • have a @:genericBuild class Transactional<Db> {} (or maybe you can think of a better name) that takes a database and produces a type with everything in Db except the transaction method, which is most easily generated as an abstract over the specific database class that @:forwards everything except the transaction method (and perhaps even add a @:forwardStatics, although the statics can be access through the original class)
  • make Database work as before and generate transaction as Transactional<CurrentDb>->Promise<...>

This would avoid the breaking change and still address your concerns, right? If not, disregard that and feel free to merge ;)

@kevinresol
Copy link
Member Author

kevinresol commented Apr 23, 2021

That will work. But there is a subtle change in the implementation detail I didn't mention. It is about the

// defintion:
interface Def extends DatabaseDefinition {}

// instance: Database<Def> generates the following
class Transaction0 implements Def {
  function new(cnx:Connection);
}
class Database0 extends Transaction0 {
  final pool:ConnectionPool; // note that ConnectionPool extends Connection and provides an additional `isolate()` method
  function new(driver)
    super(pool = driver.open());

  public function transaction(run:Db->Promise<...>)
    // create a new Transaction object and do stuff with it
    new Transaction0(pool.isolate()); // the actual code regarding isolate() is slightly more complex 
}

If we want to stick with the old approach, then the constructor of the built Database instance has to take Driver, so as the proposed Transactional abstract. But it actually just wants a Connection, in that case we can work around it by declaring a dummy Driver that will just return a constant Connection upon calling open(). But I think that is not so "clean" just to avoid a rather easy-to-migrate (IMO) breaking change.

@back2dos
Copy link
Member

But I think that is not so "clean" just to avoid a rather easy-to-migrate (IMO) breaking change.

Hmm, I'd be inclined to use an abstract over Connection that has a @:from Driver calling open(), but unfortunately I'm quite uninvolved in this lib, so I'll leave it for you to judge ;)

# Conflicts:
#	src/tink/sql/drivers/node/MySql.hx
#	src/tink/sql/drivers/node/PostgreSql.hx
#	src/tink/sql/drivers/php/PDO.hx
#	src/tink/sql/drivers/sys/StdConnection.hx
#	tests/Db.hx
#	tests/Run.hx
@kevinresol kevinresol merged commit a2d2770 into haxetink:master Sep 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants