-
Notifications
You must be signed in to change notification settings - Fork 365
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
adding support for MariaDB dialect #2521
Conversation
introduce mariadb dialect
add OnConflictUpdate support for MariaDB
@@ -321,22 +321,40 @@ enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> { | |||
// ignore: unnecessary_cast | |||
switch (this as DriftSqlType<Object>) { | |||
case DriftSqlType.bool: | |||
return dialect == SqlDialect.sqlite ? 'INTEGER' : 'boolean'; | |||
return dialect == SqlDialect.sqlite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should make these fields on the SqlDialect
enum to avoid the chain of ternary expressions here?
Then it could just be dialect.typeForBool
, for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea, could you give me some sample code? Not sure where I would put the checks inside of the enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could define SqlType
like this:
enum SqlDialect {
/// Use sqlite's sql dialect. This is the default option and the only
/// officially supported dialect at the moment.
sqlite(
textType: 'TEXT',
integerType: 'INTEGER',
realType: 'REAL',
blobType: 'BLOB',
),
/// PostgreSQL (currently supported in an experimental state)
postgres(
booleanType: 'boolean',
textType: 'text',
integerType: 'bigint',
blobType: 'bytea',
realType: 'float8',
);
final String? booleanType;
final String textType;
final String integerType;
final String realType;
final String blobType;
const SqlDialect({
required this.textType,
required this.integerType,
required this.realType,
required this.blobType,
this.booleanType,
});
}
And then do something like this in mapping.dart
:
/// Returns a suitable representation of this type in SQL.
String sqlTypeName(GenerationContext context) {
final dialect = context.dialect;
// ignore: unnecessary_cast
switch (this as DriftSqlType<Object>) {
case DriftSqlType.bool:
return dialect.booleanType ?? dialect.integerType;
// other cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, that's nice and clean. I will try it tomorrow since it's late now.
By the way, case DriftSqlType.bigInt:
is empty and goes to case DriftSqlType.int:
, MariaDB has both int
and bigInt
, which one would you recommend we use? I see in postgre bigint
was used. In my last commit I set for, see if you think it's ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be aware that should some dialect require a 3rd option for some type, like
dialect.booleanType ?? dialect == SqlDialect.mysql? 'smeTypeLiteral': 'someOtherTypeLiteral'
we'll probably have to use a ternary expression for it but I think that's ok and probably won't happen since null
will be rare I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, case DriftSqlType.bigInt: is empty and goes to case DriftSqlType.int:, MariaDB has both int and bigInt, which one would you recommend we use?
I see how this is a bit confusing, but drift actually only supports one integer type in the database: 64-bit signed integers, which I think is a bigint
in MariaDb as well. The only difference between int
and bigInt
in drift is that the later gets mapped to a BigInt
in Dart. This can make a difference for web apps, since a Dart int
on the web is just a double under a trench coat and can't represent large integers properly. But in the database, the expectation is that DriftSqlType.int
and DriftSqlType.bigInt
both represent the same type.
We should be aware that should some dialect require a 3rd option for some type, like
dialect.booleanType ?? dialect == SqlDialect.mysql? 'smeTypeLiteral': 'someOtherTypeLiteral'
Right, but we could just set the booleanType
property for the affected dialect then, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but drift actually only supports one integer type in the database: 64-bit signed integers, which I think is a bigint in MariaDb as well
correct, ok so I'll remove the bigIntType
I added and use BIGINT
for mariadb got it.
Right, but we could just set the booleanType property for the affected dialect then, right?
I'm talking about a case of a 2nd different fallback.
case DriftSqlType.bool:
return dialect.booleanType ?? dialect.integerType;
we did this, but now imagine some dialect doesn't have a boolean like sqlite, and it uses dialect.textType
instead, then it would become something like this:
case DriftSqlType.bool:
return dialect.booleanType ?? dialect == SqlDialect.someDialect ? dialect.textType : dialect.integerType;
This is obviously an extreme example and it doesn't even make sense for bools sepcifically, but could occur for some other type (although I doubt it since the dialects aren't that many, and their differences aren't that great either so I'm just thinking of an extremely unlikely scenario)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess we could cheat and set booleanType: 'TEXT'
for that dialect. But then, maybe we just do that for sqlite3 as well. Let's make booleanType
non-nullable and sett it to INT
for the sqlite3
dialect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great idea since it will always have to be something
…eck constraint for mariadb DO NOT MERGE THIS since Column.escapedName is hardcoded for mariadb dialect!
could you check 82a8f84, I feel like we should add some mariadb-specific documentation as to why this |
Good idea, and tests are an excellent way to ensure this :) Could you add something like this in test('avoids generating empty tuples', () {
// Some dialects don't support the `x IS IN ()` form, so we should avoid
// it and replace it with the direct constant (since nothing can be a
// member of the empty set). sqlite3 seems to do the same thing, as
// `NULL IN ()` is `0` and not `NULL`.
expect(innerExpression.isIn([]), generates('FALSE'));
expect(innerExpression.isNotIn([]), generates('TRUE'));
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should move a bunch of duplicated logic checking for mariadb
to common classes, but otherwise this looks great!
Do you want to contribute a package like drift_mariadb
in the extras/
folder as well? I think it would make it easier for others to use drift with MariaDb. With such package, we (I'm very happy to help with that) could also run integration tests for drift against MariaDb as part of the CI pipeline. This ensures that the MariaDb integration continues to work in the future.
_commitCommand = _parent._db.dialect == SqlDialect.mariadb | ||
? 'RELEASE SAVEPOINT s$depth' : 'RELEASE s$depth', | ||
_rollbackCommand = _parent._db.dialect == SqlDialect.mariadb | ||
? 'ROLLBACK TO SAVEPOINT s$depth' : 'ROLLBACK TO s$depth', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be moving these to the NoTransactionDelegate
class which is created by the connection implementation. The class could have a String Function(int depth)
field for savepoints, releases and rollbacks. Here, we could then call _startCommand = _parent._delegate.savepoint(depth)
, _commitCommand = _parent._delegate.release(depth)
and so on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, check my implementation on my last commit if you agree with it and its documentation.
if (aliasedName == entityName) { | ||
return '"$entityName"'; | ||
return dialect == SqlDialect.mariadb ? '`$entityName`' : '"$entityName"'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're using this check in a lot of places. Maybe we should have a String escapeChar
field on SqlDialect
? SqlDialect
could then have a method like this
String escape(String identifier) => '$escapeChar$identifier$escapeChar';
We could then replace usages with dialect.escape(entityName)
instead of duplicating the check everywhere. In the (unlikely) event that we'll ever have to use a third escape character, that will make it easier to migrate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had thought of that too but didn't know if I should do it. Since you thought it's a good idea too, I went ahead and did it. In generation_context
we have this now:
String identifier(String columnName) => dialect.escape(columnName);
which seems a bit redundant but not sure... probably ok tho, since it's easier to do something.x
instead of something.something.y(z)
. And for postgres I put double quotes as the escape char, I assume that's ok since I didn't find any checks for otherwise.
Also in Column.escapedName
I still can't do it other than hardcoded since not access to dialect
at this point. Only reason I pushed it hardcoded is cuz you told me to in order for you to easily find where to change it, should you choose to do it.
…fixed (currently hardcoded for mariadb)
I could copy paste And silly question, should it be: |
Yeah, a copy-paste variant of that package would be a good starting point 👍 Thanks! And |
Great cuz that's what I did haha (if you want to add stuff in the test, you are free to do so since I'm not the best at those)
Yep, agree! I went with |
Right. And I'm not as consistent as I should be with this either, but the Dart styleguide also suggests fully capitalizing two-letter acronyms, it's probably for the best. I have setup integration tests talking to MariaDb, but unfortunately they fail at the very first statement:
I'm not that familiar with MariaDb anymore, do you recognize what might be causing this? |
Ah nice, didn't know this.
should be |
I'm working with migration a bit (see 0963022), and assuming Finally in final schemaQuery = await _db.customSelect(
'SELECT type, name, sql FROM sqlite_master WHERE tbl_name = ?;',
variables: [Variable<String>(tableName)],
).get(); and I'm not sure what to do with it since I don't have the |
In
at
I even put
so it commits and drift loses the connection for that session or something? cuz in |
Not really, can you reproduce this by manually running the individual statements via the mariadb client package? Maybe it's because we're using a pool internally and the different statements are running on different TCP connections to MariaDB? That would be crazy though. FWIW, I don't think we necessarily need the |
ah you are right... I was so stuck getting this to work: await m.alterTable(TableMigration(
table,
columnTransformer: {
for (final column in dateTimeColumns)
column: DateTimeExpressions.fromUnixEpoch(column.dartCast<int>()),
},
)); using static Expression<DateTime> fromUnixEpoch(Expression<int> unixEpoch) {
return _DependingOnDateTimeExpression(
forTimestamps: unixEpoch.dartCast(),
forIsoString: FunctionCallExpression('FROM_UNIXTIME', [unixEpoch]),
);
} that I got caught up in it. Might try and figure that out with custom statements instead.
That would be crazy indeed, I did also try single connection from the package but that didn't do it so I've no idea why it ignores |
I've merged Some of the test failures need to be handled in generated code (for instance, MariaDB doesn't seem to support index variables so we can't do something like |
That's great!
Can you give me an example of when we would do this? Wouldn't we just do
Would it be easy for you to give me a code sample that doesn't work to investigate? Also, did you merge 78d6064 as well? cuz that has a hardcoded backtick in columns.dart that we don't have access to dialect... unless backtick as escape works fine in SQLite too... |
for some reason I'm running the build runner with the latest commits and suddenly the database.g.dart file is written for sqlite and not for mariadb. I'm not sure which change cause this, do you have any idea as to which one could be? It's like the writer overrides my dialect parameter set to mariadb Update: it seems like in drift_dev analysis/options.dart List<SqlDialect> get supportedDialects {
final dialects = dialect?.dialects;
final singleDialect = dialect?.dialect;
if (dialects != null) {
return dialects;
} else if (singleDialect != null) {
return [singleDialect];
} else {
return const [SqlDialect.sqlite];
}
} the problem is traced here since when I replace the else clause with |
Could you post the |
Yes, that's the idea. Unfortunately, if the user explicitly uses the same variable multiple times, like there:
We need to come up with something. My idea is to leave the query as-is for sqlite3 and postgres, but for MariaDB we need to use three distinct variables and bind the same value to all of them. This wouldn't be too hard if it was just variables, but we also support Dart components and array variables which can allocate a dynamic amount of variables. So the code needs to reflect that, which is not that easy to implement. The internal query model of the generator doesn't provide enough information for that at the moment, I'll take a look at adding support for that transformation.
drift/extras/integration_tests/drift_testcases/lib/suite/crud_tests.dart Lines 169 to 173 in 525ab27
Note: This might be fixed by simply applying the same transformation for MariaDB: drift/extras/integration_tests/drift_testcases/lib/suite/crud_tests.dart Lines 123 to 129 in 525ab27
|
I do not have a |
I think this is a consequence of #2536 then. Since most users only need sqlite3 support and since we don't have stable support for another dialect, I think the generated code should be optimized for sqlite3 only by default. When you add a targets:
$default:
builders:
drift_dev:
options:
sql:
dialects:
- sqlite # you could even remove this one and get mariadb-optimized code if you don't need sqlite3
- mariadb
Are you aware of dependency overrides? You can put this into your pubspec to make it use your drift checkout: dependency_overrides:
drift:
path: path/to/your/drift/clone/drift
drift_dev:
path: path/to/your/drift/clone/drift_dev |
ah yeah that could be it...
Absolutely agree until stable support is ensured.
You are correct, works like a charm now. I was caught off-guard cuz I didn't need it before and the code generation ran fine so it didn't cross my mind.
Well I am now 😅 thanks for the tip! |
I'm fine with merging this now since this doesn't interfere with the other dialects, but as you can see from the MariaDB integration tests, some things are still broken. Especially the transaction failures are a bit concerning. So we can merge this as it is and fix it later since it's still experimental, or if you want to you can also take a look at this before. I'm fine with either approach. |
I'm also fine with merging, I will be taking a look at those transaction errors since I haven't had an issue with normal or nested transaction, as you said it's still experimental so it should be ok, maybe we could throw in some quick documentation / TODO until the it's fixed and the tests pass? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, thanks a lot for your contribution! 🚀
@simolus3 if you could review these changes (with your help I will have more ready for review soon but I need your help for smooth implementation as per our discussion, for example still trying to figure out a smooth way to set the escape character as well as the datetimes)