mirror of https://github.com/AMT-Cheif/drift.git
Merge pull request #2521 from BananaMasterz/develop
adding support for MariaDB dialect
This commit is contained in:
commit
29ba50a0ca
|
@ -166,6 +166,13 @@ jobs:
|
|||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mariadb:
|
||||
image: mariadb
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: password
|
||||
MARIADB_DATABASE: database
|
||||
ports:
|
||||
- 3306:3306
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/prepare
|
||||
|
@ -173,7 +180,33 @@ jobs:
|
|||
dart_version: ${{ needs.setup.outputs.dart_version }}
|
||||
- run: melos bootstrap --no-flutter
|
||||
working-directory: .
|
||||
- run: tool/misc_integration_test.sh
|
||||
- name: Postgres integration tests
|
||||
working-directory: extras/drift_postgres
|
||||
run: |
|
||||
dart pub upgrade
|
||||
dart test
|
||||
- name: MariaDB integration tests
|
||||
working-directory: extras/drift_mariadb
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dart pub upgrade
|
||||
dart test
|
||||
- name: Integration test with built_value
|
||||
working-directory: examples/with_built_value
|
||||
run: |
|
||||
dart pub upgrade
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
- name: Integration test with modular generation
|
||||
working-directory: examples/modular
|
||||
run: |
|
||||
dart pub upgrade
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
dart run bin/example.dart
|
||||
- name: Integration test for migrations example
|
||||
working-directory: examples/migrations_example
|
||||
run: |
|
||||
dart pub upgrade
|
||||
dart test
|
||||
|
||||
migration_integration_tests:
|
||||
name: "Integration tests for migration tooling"
|
||||
|
|
|
@ -193,6 +193,187 @@ const additionalPostgresKeywords = <String>{
|
|||
'USER',
|
||||
};
|
||||
|
||||
/// A set of keywords that need to be escaped on mariadb and aren't contained
|
||||
/// in [baseKeywords].
|
||||
const additionalMariaDBKeywords = <String>{
|
||||
'ACCESSIBLE',
|
||||
'ASENSITIVE',
|
||||
'AUTO_INCREMENT',
|
||||
'BIGINT',
|
||||
'BINARY',
|
||||
'BLOB',
|
||||
'BOTH',
|
||||
'CALL',
|
||||
'CHANGE',
|
||||
'CHAR',
|
||||
'CHARACTER',
|
||||
'CONDITION',
|
||||
'CONTINUE',
|
||||
'CONVERT',
|
||||
'CURRENT_ROLE',
|
||||
'CURRENT_USER',
|
||||
'CURSOR',
|
||||
'DATABASES',
|
||||
'DAY_HOUR',
|
||||
'DAY_MICROSECOND',
|
||||
'DAY_MINUTE',
|
||||
'DAY_SECOND',
|
||||
'DEC',
|
||||
'DECIMAL',
|
||||
'DECLARE',
|
||||
'DELAYED',
|
||||
'DELETE_DOMAIN_ID',
|
||||
'DESCRIBE',
|
||||
'DETERMINISTIC',
|
||||
'DISTINCTROW',
|
||||
'DIV',
|
||||
'DO_DOMAIN_IDS',
|
||||
'DOUBLE',
|
||||
'DUAL',
|
||||
'ELSEIF',
|
||||
'ENCLOSED',
|
||||
'ESCAPED',
|
||||
'EXIT',
|
||||
'FETCH',
|
||||
'FLOAT',
|
||||
'FLOAT4',
|
||||
'FLOAT8',
|
||||
'FORCE',
|
||||
'FULLTEXT',
|
||||
'GENERAL',
|
||||
'GRANT',
|
||||
'HIGH_PRIORITY',
|
||||
'HOUR_MICROSECOND',
|
||||
'HOUR_MINUTE',
|
||||
'HOUR_SECOND',
|
||||
'IGNORE_DOMAIN_IDS',
|
||||
'IGNORE_SERVER_IDS',
|
||||
'INFILE',
|
||||
'INOUT',
|
||||
'INSENSITIVE',
|
||||
'INT',
|
||||
'INT1',
|
||||
'INT2',
|
||||
'INT3',
|
||||
'INT4',
|
||||
'INT8',
|
||||
'INTEGER',
|
||||
'INTERVAL',
|
||||
'ITERATE',
|
||||
'KEYS',
|
||||
'KILL',
|
||||
'LEADING',
|
||||
'LEAVE',
|
||||
'LINEAR',
|
||||
'LINES',
|
||||
'LOAD',
|
||||
'LOCALTIME',
|
||||
'LOCALTIMESTAMP',
|
||||
'LOCK',
|
||||
'LONG',
|
||||
'LONGBLOB',
|
||||
'LONGTEXT',
|
||||
'LOOP',
|
||||
'LOW_PRIORITY',
|
||||
'MASTER_HEARTBEAT_PERIOD',
|
||||
'MASTER_SSL_VERIFY_SERVER_CERT',
|
||||
'MAXVALUE',
|
||||
'MEDIUMBLOB',
|
||||
'MEDIUMINT',
|
||||
'MEDIUMTEXT',
|
||||
'MIDDLEINT',
|
||||
'MINUTE_MICROSECOND',
|
||||
'MINUTE_SECOND',
|
||||
'MOD',
|
||||
'MODIFIES',
|
||||
'NO_WRITE_TO_BINLOG',
|
||||
'NUMERIC',
|
||||
'OPTIMIZE',
|
||||
'OPTION',
|
||||
'OPTIONALLY',
|
||||
'OUT',
|
||||
'OUTFILE',
|
||||
'PAGE_CHECKSUM',
|
||||
'PARSE_VCOL_EXPR',
|
||||
'POSITION',
|
||||
'PRECISION',
|
||||
'PROCEDURE',
|
||||
'PURGE',
|
||||
'READ',
|
||||
'READS',
|
||||
'READ_WRITE',
|
||||
'REAL',
|
||||
'REF_SYSTEM_ID',
|
||||
'REPEAT',
|
||||
'REQUIRE',
|
||||
'RESIGNAL',
|
||||
'RETURN',
|
||||
'RETURNING',
|
||||
'REVOKE',
|
||||
'RLIKE',
|
||||
'ROW_NUMBER',
|
||||
'SCHEMA',
|
||||
'SCHEMAS',
|
||||
'SECOND_MICROSECOND',
|
||||
'SENSITIVE',
|
||||
'SEPARATOR',
|
||||
'SHOW',
|
||||
'SIGNAL',
|
||||
'SLOW',
|
||||
'SMALLINT',
|
||||
'SPATIAL',
|
||||
'SPECIFIC',
|
||||
'SQL',
|
||||
'SQLEXCEPTION',
|
||||
'SQLSTATE',
|
||||
'SQLWARNING',
|
||||
'SQL_BIG_RESULT',
|
||||
'SQL_CALC_FOUND_ROWS',
|
||||
'SQL_SMALL_RESULT',
|
||||
'SSL',
|
||||
'STARTING',
|
||||
'STATS_AUTO_RECALC',
|
||||
'STATS_PERSISTENT',
|
||||
'STATS_SAMPLE_PAGES',
|
||||
'STRAIGHT_JOIN',
|
||||
'TERMINATED',
|
||||
'TINYBLOB',
|
||||
'TINYINT',
|
||||
'TINYTEXT',
|
||||
'TRAILING',
|
||||
'TYPE',
|
||||
'UNDO',
|
||||
'UNLOCK',
|
||||
'UNSIGNED',
|
||||
'USAGE',
|
||||
'USE',
|
||||
'UTC_DATE',
|
||||
'UTC_TIME',
|
||||
'UTC_TIMESTAMP',
|
||||
'VARBINARY',
|
||||
'VARCHAR',
|
||||
'VARCHARACTER',
|
||||
'VARYING',
|
||||
'WHILE',
|
||||
'WRITE',
|
||||
'XOR',
|
||||
'YEAR_MONTH',
|
||||
'ZEROFILL',
|
||||
'BODY',
|
||||
'ELSIF',
|
||||
'GOTO',
|
||||
'HISTORY',
|
||||
'MINUS',
|
||||
'PACKAGE',
|
||||
'PERIOD',
|
||||
'ROWNUM',
|
||||
'ROWTYPE',
|
||||
'SYSDATE',
|
||||
'SYSTEM',
|
||||
'SYSTEM_TIME',
|
||||
'VERSIONING',
|
||||
};
|
||||
|
||||
/// Returns whether [s] is an sql keyword by comparing it to the
|
||||
/// [sqliteKeywords].
|
||||
bool isSqliteKeyword(String s) => sqliteKeywords.contains(s.toUpperCase());
|
||||
|
@ -208,6 +389,11 @@ String escapeIfNeeded(String s, [SqlDialect dialect = SqlDialect.sqlite]) {
|
|||
isKeyword |= additionalPostgresKeywords.contains(inUpperCase);
|
||||
}
|
||||
|
||||
if (isKeyword || _notInKeyword.hasMatch(s)) return '"$s"';
|
||||
if (dialect == SqlDialect.mariadb) {
|
||||
isKeyword |= additionalMariaDBKeywords.contains(inUpperCase);
|
||||
}
|
||||
|
||||
if (isKeyword || _notInKeyword.hasMatch(s)) return dialect.escape(s);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ enum KeyAction {
|
|||
|
||||
/// No special action is taken when the parent key is modified or deleted from
|
||||
/// the database.
|
||||
///
|
||||
/// For [SqlDialect.mariadb] this is synonym for [restrict].
|
||||
noAction,
|
||||
}
|
||||
|
||||
|
@ -34,7 +36,7 @@ abstract class Column<T extends Object> extends Expression<T> {
|
|||
|
||||
/// The (unescaped) name of this column.
|
||||
///
|
||||
/// Use [escapedName] to access a name that's escaped in double quotes if
|
||||
/// Use [escapedNameFor] to access a name that's escaped in double quotes if
|
||||
/// needed.
|
||||
String get name;
|
||||
|
||||
|
@ -43,7 +45,14 @@ abstract class Column<T extends Object> extends Expression<T> {
|
|||
/// In the past, this getter only used to add double-quotes when that is
|
||||
/// really needed (for instance because [name] is also a reserved keyword).
|
||||
/// For performance reasons, we unconditionally escape names now.
|
||||
@Deprecated('Use escapedNameFor with the current dialect')
|
||||
String get escapedName => '"$name"';
|
||||
|
||||
/// [name], but wrapped in double quotes or the DBMS-specific escape
|
||||
/// identifier.
|
||||
String escapedNameFor(SqlDialect dialect) {
|
||||
return dialect.escape(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// A column that stores int values.
|
||||
|
|
|
@ -566,10 +566,21 @@ abstract class DatabaseConnectionUser {
|
|||
/// Used by generated code to expand array variables.
|
||||
String $expandVar(int start, int amount) {
|
||||
final buffer = StringBuffer();
|
||||
final mark = executor.dialect == SqlDialect.postgres ? '@' : '?';
|
||||
|
||||
final variableSymbol = switch (executor.dialect) {
|
||||
SqlDialect.postgres => r'$',
|
||||
_ => '?',
|
||||
};
|
||||
final supportsIndexedParameters =
|
||||
executor.dialect.supportsIndexedParameters;
|
||||
|
||||
for (var x = 0; x < amount; x++) {
|
||||
buffer.write('$mark${start + x}');
|
||||
if (supportsIndexedParameters) {
|
||||
buffer.write('$variableSymbol${start + x}');
|
||||
} else {
|
||||
buffer.write(variableSymbol);
|
||||
}
|
||||
|
||||
if (x != amount - 1) {
|
||||
buffer.write(', ');
|
||||
}
|
||||
|
|
|
@ -3,6 +3,12 @@ import 'dart:async' show FutureOr;
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/runtime/executor/helpers/results.dart';
|
||||
|
||||
String _defaultSavepoint(int depth) => 'SAVEPOINT s$depth';
|
||||
|
||||
String _defaultRelease(int depth) => 'RELEASE s$depth';
|
||||
|
||||
String _defaultRollbackToSavepoint(int depth) => 'ROLLBACK TO s$depth';
|
||||
|
||||
/// An interface that supports sending database queries. Used as a backend for
|
||||
/// drift.
|
||||
///
|
||||
|
@ -132,6 +138,18 @@ class NoTransactionDelegate extends TransactionDelegate {
|
|||
/// database engine.
|
||||
final String rollback;
|
||||
|
||||
/// The statement that will create a savepoint for a given depth of a transaction
|
||||
/// on this database engine.
|
||||
final String Function(int depth) savepoint;
|
||||
|
||||
/// The statement that will release a savepoint for a given depth of a transaction
|
||||
/// on this database engine.
|
||||
final String Function(int depth) release;
|
||||
|
||||
/// The statement that will perform a rollback to a savepoint for a given depth
|
||||
/// of a transaction on this database engine.
|
||||
final String Function(int depth) rollbackToSavepoint;
|
||||
|
||||
/// Construct a transaction delegate indicating that native transactions
|
||||
/// aren't supported and need to be emulated by issuing statements and
|
||||
/// locking the database.
|
||||
|
@ -139,6 +157,9 @@ class NoTransactionDelegate extends TransactionDelegate {
|
|||
this.start = 'BEGIN TRANSACTION',
|
||||
this.commit = 'COMMIT TRANSACTION',
|
||||
this.rollback = 'ROLLBACK TRANSACTION',
|
||||
this.savepoint = _defaultSavepoint,
|
||||
this.release = _defaultRelease,
|
||||
this.rollbackToSavepoint = _defaultRollbackToSavepoint,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -187,9 +187,9 @@ class _StatementBasedTransactionExecutor extends _TransactionExecutor {
|
|||
_StatementBasedTransactionExecutor.nested(
|
||||
_StatementBasedTransactionExecutor this._parent, int depth)
|
||||
: _delegate = _parent._delegate,
|
||||
_startCommand = 'SAVEPOINT s$depth',
|
||||
_commitCommand = 'RELEASE s$depth',
|
||||
_rollbackCommand = 'ROLLBACK TO s$depth',
|
||||
_startCommand = _parent._delegate.savepoint(depth),
|
||||
_commitCommand = _parent._delegate.release(depth),
|
||||
_rollbackCommand = _parent._delegate.rollbackToSavepoint(depth),
|
||||
super(_parent._db);
|
||||
|
||||
@override
|
||||
|
|
|
@ -156,6 +156,10 @@ abstract class Expression<D extends Object> implements FunctionParameter {
|
|||
///
|
||||
/// For an "is in" comparison with values, use [isIn].
|
||||
Expression<bool> isInExp(List<Expression<D>> expressions) {
|
||||
if (expressions.isEmpty) {
|
||||
return Constant(false);
|
||||
}
|
||||
|
||||
return _InExpression(this, expressions, false);
|
||||
}
|
||||
|
||||
|
@ -164,6 +168,10 @@ abstract class Expression<D extends Object> implements FunctionParameter {
|
|||
///
|
||||
/// For an "is not in" comparison with values, use [isNotIn].
|
||||
Expression<bool> isNotInExp(List<Expression<D>> expressions) {
|
||||
if (expressions.isEmpty) {
|
||||
return Constant(true);
|
||||
}
|
||||
|
||||
return _InExpression(this, expressions, true);
|
||||
}
|
||||
|
||||
|
@ -472,10 +480,35 @@ class _CastInSqlExpression<D1 extends Object, D2 extends Object>
|
|||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
final type = DriftSqlType.forType<D2>();
|
||||
if (type == DriftSqlType.any) {
|
||||
inner.writeInto(context); // No need to cast
|
||||
}
|
||||
|
||||
final String typeName;
|
||||
|
||||
if (context.dialect == SqlDialect.mariadb) {
|
||||
// MariaDB has a weird cast syntax that uses different type names than the
|
||||
// ones used in a create table statement.
|
||||
|
||||
// ignore: unnecessary_cast
|
||||
typeName = switch (type as DriftSqlType<Object>) {
|
||||
DriftSqlType.int ||
|
||||
DriftSqlType.bigInt ||
|
||||
DriftSqlType.bool =>
|
||||
'INTEGER',
|
||||
DriftSqlType.string => 'CHAR',
|
||||
DriftSqlType.double => 'DOUBLE',
|
||||
DriftSqlType.blob => 'BINARY',
|
||||
DriftSqlType.dateTime => 'DATETIME',
|
||||
DriftSqlType.any => '',
|
||||
};
|
||||
} else {
|
||||
typeName = type.sqlTypeName(context);
|
||||
}
|
||||
|
||||
context.buffer.write('CAST(');
|
||||
inner.writeInto(context);
|
||||
context.buffer.write(' AS ${type.sqlTypeName(context)})');
|
||||
context.buffer.write(' AS $typeName)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,15 @@ class GenerationContext {
|
|||
/// Gets the generated sql statement
|
||||
String get sql => buffer.toString();
|
||||
|
||||
/// The variable indices occupied by this generation context.
|
||||
///
|
||||
/// SQL variables are 1-indexed, so a context with three variables would
|
||||
/// cover the variables `1`, `2` and `3` by default.
|
||||
Iterable<int> get variableIndices {
|
||||
final start = explicitVariableIndex ?? 1;
|
||||
return Iterable.generate(amountOfVariables, (i) => start + i);
|
||||
}
|
||||
|
||||
/// Constructs a [GenerationContext] by copying the relevant fields from the
|
||||
/// database.
|
||||
GenerationContext.fromDb(DatabaseConnectionUser this.executor,
|
||||
|
@ -87,6 +96,6 @@ class GenerationContext {
|
|||
void writeWhitespace() => buffer.write(' ');
|
||||
|
||||
/// Turns [columnName] into a safe SQL identifier by wrapping it in double
|
||||
/// quotes.
|
||||
String identifier(String columnName) => '"$columnName"';
|
||||
/// quotes, or backticks depending on the dialect.
|
||||
String identifier(String columnName) => dialect.escape(columnName);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ typedef OnUpgrade = Future<void> Function(Migrator m, int from, int to);
|
|||
typedef OnBeforeOpen = Future<void> Function(OpeningDetails details);
|
||||
|
||||
Future<void> _defaultOnCreate(Migrator m) => m.createAll();
|
||||
|
||||
Future<void> _defaultOnUpdate(Migrator m, int from, int to) async =>
|
||||
throw Exception("You've bumped the schema version for your drift database "
|
||||
"but didn't provide a strategy for schema updates. Please do that by "
|
||||
|
@ -227,7 +228,7 @@ class Migrator {
|
|||
expressionsForSelect.add(expression);
|
||||
|
||||
if (!first) context.buffer.write(', ');
|
||||
context.buffer.write(column.escapedName);
|
||||
context.buffer.write(column.escapedNameFor(context.dialect));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +309,7 @@ class Migrator {
|
|||
for (var i = 0; i < pkList.length; i++) {
|
||||
final column = pkList[i];
|
||||
|
||||
context.buffer.write(column.escapedName);
|
||||
context.buffer.write(column.escapedNameFor(context.dialect));
|
||||
|
||||
if (i != pkList.length - 1) context.buffer.write(', ');
|
||||
}
|
||||
|
@ -322,7 +323,7 @@ class Migrator {
|
|||
for (var i = 0; i < uqList.length; i++) {
|
||||
final column = uqList[i];
|
||||
|
||||
context.buffer.write(column.escapedName);
|
||||
context.buffer.write(column.escapedNameFor(context.dialect));
|
||||
|
||||
if (i != uqList.length - 1) context.buffer.write(', ');
|
||||
}
|
||||
|
@ -378,7 +379,9 @@ class Migrator {
|
|||
await _issueQueryByDialect(stmts);
|
||||
} else if (view.query != null) {
|
||||
final context = GenerationContext.fromDb(_db, supportsVariables: false);
|
||||
final columnNames = view.$columns.map((e) => e.escapedName).join(', ');
|
||||
final columnNames = view.$columns
|
||||
.map((e) => e.escapedNameFor(context.dialect))
|
||||
.join(', ');
|
||||
|
||||
context.generatingForView = view.entityName;
|
||||
context.buffer.write('CREATE VIEW IF NOT EXISTS '
|
||||
|
@ -447,14 +450,15 @@ class Migrator {
|
|||
/// in sqlite version 3.25.0, released on 2018-09-15. When you're using
|
||||
/// Flutter and depend on `sqlite3_flutter_libs`, you're guaranteed to have
|
||||
/// that version. Otherwise, please ensure that you only use [renameColumn] if
|
||||
/// you know you'll run on sqlite 3.20.0 or later.
|
||||
/// you know you'll run on sqlite 3.20.0 or later. In MariaDB support for that
|
||||
/// same syntax was added in MariaDB version 10.5.2, released on 2020-03-26.
|
||||
Future<void> renameColumn(
|
||||
TableInfo table, String oldName, GeneratedColumn column) async {
|
||||
final context = _createContext();
|
||||
context.buffer
|
||||
..write('ALTER TABLE ${context.identifier(table.aliasedName)} ')
|
||||
..write('RENAME COLUMN ${context.identifier(oldName)} ')
|
||||
..write('TO ${column.escapedName};');
|
||||
..write('TO ${column.escapedNameFor(context.dialect)};');
|
||||
|
||||
return _issueCustomQuery(context.sql);
|
||||
}
|
||||
|
@ -467,8 +471,13 @@ class Migrator {
|
|||
/// databases.
|
||||
Future<void> renameTable(TableInfo table, String oldName) async {
|
||||
final context = _createContext();
|
||||
context.buffer.write('ALTER TABLE ${context.identifier(oldName)} '
|
||||
'RENAME TO ${context.identifier(table.actualTableName)};');
|
||||
context.buffer.write(switch (context.dialect) {
|
||||
SqlDialect.mariadb => 'RENAME TABLE ${context.identifier(oldName)} '
|
||||
'TO ${context.identifier(table.actualTableName)};',
|
||||
_ => 'ALTER TABLE ${context.identifier(oldName)} '
|
||||
'RENAME TO ${context.identifier(table.actualTableName)};',
|
||||
});
|
||||
|
||||
return _issueCustomQuery(context.sql);
|
||||
}
|
||||
|
||||
|
|
|
@ -96,11 +96,107 @@ void _writeCommaSeparated(
|
|||
enum SqlDialect {
|
||||
/// Use sqlite's sql dialect. This is the default option and the only
|
||||
/// officially supported dialect at the moment.
|
||||
sqlite,
|
||||
sqlite(
|
||||
booleanType: 'INTEGER',
|
||||
textType: 'TEXT',
|
||||
integerType: 'INTEGER',
|
||||
realType: 'REAL',
|
||||
blobType: 'BLOB',
|
||||
),
|
||||
|
||||
/// (currently unsupported)
|
||||
mysql,
|
||||
@Deprecated('Use mariadb instead, even when talking to a MySQL database')
|
||||
mysql(
|
||||
booleanType: '',
|
||||
textType: '',
|
||||
integerType: '',
|
||||
blobType: '',
|
||||
realType: '',
|
||||
),
|
||||
|
||||
/// PostgreSQL (currently supported in an experimental state)
|
||||
postgres,
|
||||
postgres(
|
||||
booleanType: 'boolean',
|
||||
textType: 'text',
|
||||
integerType: 'bigint',
|
||||
blobType: 'bytea',
|
||||
realType: 'float8',
|
||||
),
|
||||
|
||||
/// MariaDB (currently supported in an experimental state)
|
||||
mariadb(
|
||||
booleanType: 'BOOLEAN',
|
||||
textType: 'TEXT',
|
||||
integerType: 'BIGINT',
|
||||
blobType: 'BLOB',
|
||||
realType: 'DOUBLE',
|
||||
escapeChar: '`',
|
||||
supportsIndexedParameters: false,
|
||||
);
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store booleans.
|
||||
final String booleanType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store strings.
|
||||
final String textType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store 64-bit
|
||||
/// integers.
|
||||
final String integerType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store doubles.
|
||||
final String realType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store blobs (as
|
||||
/// a [Uint8List] in Dart).
|
||||
final String blobType;
|
||||
|
||||
/// The character used to wrap identifiers to distinguish them from keywords.
|
||||
///
|
||||
/// This is a double quote character in ANSI SQL, but MariaDB uses backticks
|
||||
/// by default.
|
||||
final String escapeChar;
|
||||
|
||||
/// Whether this dialect supports indexed parameters.
|
||||
///
|
||||
/// For dialects that support this features, an explicit index can be given
|
||||
/// for parameters, even if it doesn't match the order of occurrences in the
|
||||
/// given statement (e.g. `INSERT INTO foo VALUES (?1, ?2, ?3, ?4)`).
|
||||
/// In dialects without this feature, every syntactic occurrence of a variable
|
||||
/// introduces a new logical variable with a new index, variables also can't
|
||||
/// be re-used.
|
||||
final bool supportsIndexedParameters;
|
||||
|
||||
/// Escapes [identifier] by wrapping it in [escapeChar].
|
||||
String escape(String identifier) => '$escapeChar$identifier$escapeChar';
|
||||
|
||||
const SqlDialect({
|
||||
required this.booleanType,
|
||||
required this.textType,
|
||||
required this.integerType,
|
||||
required this.realType,
|
||||
required this.blobType,
|
||||
this.escapeChar = '"',
|
||||
this.supportsIndexedParameters = true,
|
||||
});
|
||||
|
||||
/// For dialects that don't support named or explicitly-indexed variables,
|
||||
/// translates a variable assignment to avoid using that feature.
|
||||
///
|
||||
/// For instance, the SQL snippet `WHERE x = :a OR y = :a` would be translated
|
||||
/// to `WHERE x = ? OR y = ?`. Then, [original] would contain the value for
|
||||
/// the single variable and [syntacticOccurences] would contain two values
|
||||
/// (`1` and `1`) referencing the original variable.
|
||||
List<Variable> desugarDuplicateVariables(
|
||||
List<Variable> original,
|
||||
List<int> syntacticOccurences,
|
||||
) {
|
||||
if (supportsIndexedParameters) return original;
|
||||
|
||||
return [
|
||||
for (final occurence in syntacticOccurences)
|
||||
// Variables in SQL are 1-indexed
|
||||
original[occurence - 1],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,6 +115,7 @@ class GeneratedColumn<T extends Object> extends Column<T> {
|
|||
/// buffer.
|
||||
void writeColumnDefinition(GenerationContext into) {
|
||||
final isSerial = into.dialect == SqlDialect.postgres && hasAutoIncrement;
|
||||
final escapedName = escapedNameFor(into.dialect);
|
||||
|
||||
if (isSerial) {
|
||||
into.buffer.write('$escapedName bigserial PRIMARY KEY NOT NULL');
|
||||
|
@ -177,7 +178,8 @@ class GeneratedColumn<T extends Object> extends Column<T> {
|
|||
..write(context.identifier(tableName))
|
||||
..write('.');
|
||||
}
|
||||
context.buffer.write(ignoreEscape ? $name : escapedName);
|
||||
context.buffer
|
||||
.write(ignoreEscape ? $name : escapedNameFor(context.dialect));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -177,10 +177,13 @@ extension NameWithAlias on ResultSetImplementation<dynamic, dynamic> {
|
|||
/// can be used in select statements, as it returns something like "users u"
|
||||
/// for a table called users that has been aliased as "u".
|
||||
String get tableWithAlias {
|
||||
var dialect = attachedDatabase.executor.dialect;
|
||||
var entityNameEscaped = dialect.escape(entityName);
|
||||
var aliasedNameEscaped = dialect.escape(aliasedName);
|
||||
if (aliasedName == entityName) {
|
||||
return '"$entityName"';
|
||||
return entityNameEscaped;
|
||||
} else {
|
||||
return '"$entityName" "$aliasedName"';
|
||||
return '$entityNameEscaped $aliasedNameEscaped';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,27 +273,31 @@ class InsertStatement<T extends Table, D> {
|
|||
) {
|
||||
void writeOnConflictConstraint(
|
||||
List<Column<Object>>? target, Expression<bool>? where) {
|
||||
ctx.buffer.write(' ON CONFLICT(');
|
||||
if (ctx.dialect == SqlDialect.mariadb) {
|
||||
ctx.buffer.write(' ON DUPLICATE');
|
||||
} else {
|
||||
ctx.buffer.write(' ON CONFLICT(');
|
||||
|
||||
final conflictTarget = target ?? table.$primaryKey.toList();
|
||||
final conflictTarget = target ?? table.$primaryKey.toList();
|
||||
|
||||
if (conflictTarget.isEmpty) {
|
||||
throw ArgumentError(
|
||||
'Table has no primary key, so a conflict target is needed.');
|
||||
if (conflictTarget.isEmpty) {
|
||||
throw ArgumentError(
|
||||
'Table has no primary key, so a conflict target is needed.');
|
||||
}
|
||||
|
||||
var first = true;
|
||||
for (final target in conflictTarget) {
|
||||
if (!first) ctx.buffer.write(', ');
|
||||
|
||||
// Writing the escaped name directly because it should not have a table
|
||||
// name in front of it.
|
||||
ctx.buffer.write(target.escapedNameFor(ctx.dialect));
|
||||
first = false;
|
||||
}
|
||||
|
||||
ctx.buffer.write(')');
|
||||
}
|
||||
|
||||
var first = true;
|
||||
for (final target in conflictTarget) {
|
||||
if (!first) ctx.buffer.write(', ');
|
||||
|
||||
// Writing the escaped name directly because it should not have a table
|
||||
// name in front of it.
|
||||
ctx.buffer.write(target.escapedName);
|
||||
first = false;
|
||||
}
|
||||
|
||||
ctx.buffer.write(')');
|
||||
|
||||
if (where != null) {
|
||||
Where(where).writeInto(ctx);
|
||||
}
|
||||
|
@ -325,7 +329,11 @@ class InsertStatement<T extends Table, D> {
|
|||
mode == InsertMode.insertOrIgnore) {
|
||||
ctx.buffer.write(' DO NOTHING ');
|
||||
} else {
|
||||
ctx.buffer.write(' DO UPDATE SET ');
|
||||
if (ctx.dialect == SqlDialect.mariadb) {
|
||||
ctx.buffer.write(' KEY UPDATE ');
|
||||
} else {
|
||||
ctx.buffer.write(' DO UPDATE SET ');
|
||||
}
|
||||
|
||||
var first = true;
|
||||
for (final update in updateSet.entries) {
|
||||
|
|
|
@ -128,11 +128,12 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
chosenAlias = _columnAliases[column]!;
|
||||
}
|
||||
|
||||
final chosenAliasEscaped = ctx.dialect.escape(chosenAlias);
|
||||
|
||||
column.writeInto(ctx);
|
||||
ctx.buffer
|
||||
..write(' AS "')
|
||||
..write(chosenAlias)
|
||||
..write('"');
|
||||
..write(' AS ')
|
||||
..write(chosenAliasEscaped);
|
||||
}
|
||||
|
||||
ctx.buffer.write(' FROM ');
|
||||
|
|
|
@ -321,22 +321,22 @@ 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.booleanType;
|
||||
case DriftSqlType.string:
|
||||
return dialect == SqlDialect.sqlite ? 'TEXT' : 'text';
|
||||
return dialect.textType;
|
||||
case DriftSqlType.bigInt:
|
||||
case DriftSqlType.int:
|
||||
return dialect == SqlDialect.sqlite ? 'INTEGER' : 'bigint';
|
||||
return dialect.integerType;
|
||||
case DriftSqlType.dateTime:
|
||||
if (context.typeMapping.storeDateTimesAsText) {
|
||||
return dialect == SqlDialect.sqlite ? 'TEXT' : 'text';
|
||||
return dialect.textType;
|
||||
} else {
|
||||
return dialect == SqlDialect.sqlite ? 'INTEGER' : 'bigint';
|
||||
return dialect.integerType;
|
||||
}
|
||||
case DriftSqlType.blob:
|
||||
return dialect == SqlDialect.sqlite ? 'BLOB' : 'bytea';
|
||||
return dialect.blobType;
|
||||
case DriftSqlType.double:
|
||||
return dialect == SqlDialect.sqlite ? 'REAL' : 'float8';
|
||||
return dialect.realType;
|
||||
case DriftSqlType.any:
|
||||
return 'ANY';
|
||||
}
|
||||
|
|
|
@ -67,5 +67,14 @@ void main() {
|
|||
generates(
|
||||
'name NOT IN (SELECT "users"."name" AS "users.name" FROM "users")'));
|
||||
});
|
||||
|
||||
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('0'));
|
||||
expect(innerExpression.isNotIn([]), generates('1'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import 'required_variables.dart';
|
|||
/// class is simply there to bundle the data.
|
||||
class _QueryHandlerContext {
|
||||
final List<FoundElement> foundElements;
|
||||
final List<SyntacticElementReference> elementReferences;
|
||||
final AstNode root;
|
||||
final NestedQueriesContainer? nestedScope;
|
||||
final String queryName;
|
||||
|
@ -32,13 +33,15 @@ class _QueryHandlerContext {
|
|||
|
||||
_QueryHandlerContext({
|
||||
required List<FoundElement> foundElements,
|
||||
required List<SyntacticElementReference> elementReferences,
|
||||
required this.root,
|
||||
required this.queryName,
|
||||
required this.nestedScope,
|
||||
this.requestedResultClass,
|
||||
this.requestedResultType,
|
||||
this.sourceForFixedName,
|
||||
}) : foundElements = List.unmodifiable(foundElements);
|
||||
}) : foundElements = List.unmodifiable(foundElements),
|
||||
elementReferences = List.unmodifiable(elementReferences);
|
||||
}
|
||||
|
||||
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
|
||||
|
@ -103,7 +106,7 @@ class QueryAnalyzer {
|
|||
nestedScope = nestedAnalyzer.analyzeRoot(context.root as SelectStatement);
|
||||
}
|
||||
|
||||
final foundElements = _extractElements(
|
||||
final (foundElements, references) = _extractElements(
|
||||
ctx: context,
|
||||
root: context.root,
|
||||
required: requiredVariables,
|
||||
|
@ -120,6 +123,7 @@ class QueryAnalyzer {
|
|||
|
||||
final query = _mapToDrift(_QueryHandlerContext(
|
||||
foundElements: foundElements,
|
||||
elementReferences: references,
|
||||
queryName: declaration.name,
|
||||
requestedResultClass: requestedResultClass,
|
||||
requestedResultType: requestedResultType,
|
||||
|
@ -209,6 +213,7 @@ class QueryAnalyzer {
|
|||
context,
|
||||
root,
|
||||
queryContext.foundElements,
|
||||
queryContext.elementReferences,
|
||||
updatedFinder.writtenTables
|
||||
.map((write) {
|
||||
final table = _lookupReference<DriftTable?>(write.table.name);
|
||||
|
@ -265,6 +270,7 @@ class QueryAnalyzer {
|
|||
context,
|
||||
queryContext.root,
|
||||
queryContext.foundElements,
|
||||
queryContext.elementReferences,
|
||||
driftEntities,
|
||||
_inferResultSet(queryContext, resolvedColumns, syntacticColumns),
|
||||
queryContext.requestedResultClass,
|
||||
|
@ -447,6 +453,7 @@ class QueryAnalyzer {
|
|||
final driftResultSet = _inferResultSet(
|
||||
_QueryHandlerContext(
|
||||
foundElements: queryContext.foundElements,
|
||||
elementReferences: queryContext.elementReferences,
|
||||
root: queryContext.root,
|
||||
queryName: queryContext.queryName,
|
||||
nestedScope: queryContext.nestedScope,
|
||||
|
@ -484,7 +491,7 @@ class QueryAnalyzer {
|
|||
_QueryHandlerContext queryContext, NestedQueryColumn column) {
|
||||
final childScope = queryContext.nestedScope?.nestedQueries[column];
|
||||
|
||||
final foundElements = _extractElements(
|
||||
final (foundElements, references) = _extractElements(
|
||||
ctx: context,
|
||||
root: column.select,
|
||||
required: requiredVariables,
|
||||
|
@ -511,6 +518,7 @@ class QueryAnalyzer {
|
|||
requestedResultClass: resultClassName,
|
||||
root: column.select,
|
||||
foundElements: foundElements,
|
||||
elementReferences: references,
|
||||
nestedScope: childScope,
|
||||
)),
|
||||
);
|
||||
|
@ -564,7 +572,7 @@ class QueryAnalyzer {
|
|||
/// a Dart placeholder, its indexed is LOWER than that element. This means
|
||||
/// that elements can be expanded into multiple variables without breaking
|
||||
/// variables that appear after them.
|
||||
List<FoundElement> _extractElements({
|
||||
(List<FoundElement>, List<SyntacticElementReference>) _extractElements({
|
||||
required AnalysisContext ctx,
|
||||
required AstNode root,
|
||||
NestedQueriesContainer? nestedScope,
|
||||
|
@ -581,6 +589,8 @@ class QueryAnalyzer {
|
|||
final merged = _mergeVarsAndPlaceholders(variables, placeholders);
|
||||
|
||||
final foundElements = <FoundElement>[];
|
||||
final references = <SyntacticElementReference>[];
|
||||
|
||||
// we don't allow variables with an explicit index after an array. For
|
||||
// instance: SELECT * FROM t WHERE id IN ? OR id = ?2. The reason this is
|
||||
// not allowed is that we expand the first arg into multiple vars at runtime
|
||||
|
@ -589,9 +599,15 @@ class QueryAnalyzer {
|
|||
var maxIndex = 999;
|
||||
var currentIndex = 0;
|
||||
|
||||
void addNewElement(FoundElement element) {
|
||||
foundElements.add(element);
|
||||
references.add(SyntacticElementReference(element));
|
||||
}
|
||||
|
||||
for (final used in merged) {
|
||||
if (used is Variable) {
|
||||
if (used.resolvedIndex == currentIndex) {
|
||||
references.add(SyntacticElementReference(foundElements.last));
|
||||
continue; // already handled, we only report a single variable / index
|
||||
}
|
||||
|
||||
|
@ -624,7 +640,7 @@ class QueryAnalyzer {
|
|||
final type = driver.typeMapping.sqlTypeToDrift(internalType.type);
|
||||
|
||||
if (forCapture != null) {
|
||||
foundElements.add(FoundVariable.nestedQuery(
|
||||
addNewElement(FoundVariable.nestedQuery(
|
||||
index: currentIndex,
|
||||
name: name,
|
||||
sqlType: type,
|
||||
|
@ -657,7 +673,7 @@ class QueryAnalyzer {
|
|||
converter = (internalType.type!.hint as TypeConverterHint).converter;
|
||||
}
|
||||
|
||||
foundElements.add(FoundVariable(
|
||||
addNewElement(FoundVariable(
|
||||
index: currentIndex,
|
||||
name: name,
|
||||
sqlType: type,
|
||||
|
@ -684,10 +700,10 @@ class QueryAnalyzer {
|
|||
// we don't what index this placeholder has, so we can't allow _any_
|
||||
// explicitly indexed variables coming after this
|
||||
maxIndex = 0;
|
||||
foundElements.add(_extractPlaceholder(ctx, used));
|
||||
addNewElement(_extractPlaceholder(ctx, used));
|
||||
}
|
||||
}
|
||||
return foundElements;
|
||||
return (foundElements, references);
|
||||
}
|
||||
|
||||
FoundDartPlaceholder _extractPlaceholder(
|
||||
|
|
|
@ -103,6 +103,13 @@ enum QueryMode {
|
|||
atCreate,
|
||||
}
|
||||
|
||||
///A reference to a [FoundElement] occuring in the SQL query.
|
||||
class SyntacticElementReference {
|
||||
final FoundElement referencedElement;
|
||||
|
||||
SyntacticElementReference(this.referencedElement);
|
||||
}
|
||||
|
||||
/// A fully-resolved and analyzed SQL query.
|
||||
abstract class SqlQuery {
|
||||
final String name;
|
||||
|
@ -143,22 +150,44 @@ abstract class SqlQuery {
|
|||
/// if their index is lower than that of the array (e.g `a = ?2 AND b IN ?
|
||||
/// AND c IN ?1`. In other words, we can expand an array without worrying
|
||||
/// about the variables that appear after that array.
|
||||
late List<FoundVariable> variables;
|
||||
late final List<FoundVariable> variables =
|
||||
elements.whereType<FoundVariable>().toList();
|
||||
|
||||
/// The placeholders in this query which are bound and converted to sql at
|
||||
/// runtime. For instance, in `SELECT * FROM tbl WHERE $expr`, the `expr` is
|
||||
/// going to be a [FoundDartPlaceholder] with the type
|
||||
/// [ExpressionDartPlaceholderType] and [DriftSqlType.bool]. We will
|
||||
/// generate a method which has a `Expression<bool, BoolType> expr` parameter.
|
||||
late List<FoundDartPlaceholder> placeholders;
|
||||
late final List<FoundDartPlaceholder> placeholders =
|
||||
elements.whereType<FoundDartPlaceholder>().toList();
|
||||
|
||||
/// Union of [variables] and [placeholders], but in the order in which they
|
||||
/// appear inside the query.
|
||||
final List<FoundElement> elements;
|
||||
|
||||
SqlQuery(this.name, this.elements) {
|
||||
variables = elements.whereType<FoundVariable>().toList();
|
||||
placeholders = elements.whereType<FoundDartPlaceholder>().toList();
|
||||
/// All references to any [FoundElement] in [elements], but in the order in
|
||||
/// which they appear in the query.
|
||||
///
|
||||
/// This is very similar to [elements] itself, except that elements referenced
|
||||
/// multiple times are also in this list multiple times. For instance, the
|
||||
/// query `SELECT * FROM foo WHERE ?1 ORDER BY $order LIMIT ?1` would have two
|
||||
/// elements (the variable and the Dart template, in that order), but three
|
||||
/// references (the variable, the template, and then the variable again).
|
||||
final List<SyntacticElementReference> elementSources;
|
||||
|
||||
SqlQuery(this.name, this.elements, this.elementSources);
|
||||
|
||||
/// Whether any element in [elements] has more than one definite
|
||||
/// [elementSources] pointing to it.
|
||||
bool get referencesAnyElementMoreThanOnce {
|
||||
final found = <FoundElement>{};
|
||||
for (final source in elementSources) {
|
||||
if (!found.add(source.referencedElement)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get _useResultClassName {
|
||||
|
@ -239,11 +268,12 @@ class SqlSelectQuery extends SqlQuery {
|
|||
this.fromContext,
|
||||
this.root,
|
||||
List<FoundElement> elements,
|
||||
List<SyntacticElementReference> elementSources,
|
||||
this.readsFrom,
|
||||
this.resultSet,
|
||||
this.requestedResultClass,
|
||||
this.nestedContainer,
|
||||
) : super(name, elements);
|
||||
) : super(name, elements, elementSources);
|
||||
|
||||
Set<DriftTable> get readsFromTables {
|
||||
return {
|
||||
|
@ -264,6 +294,7 @@ class SqlSelectQuery extends SqlQuery {
|
|||
fromContext,
|
||||
root,
|
||||
elements,
|
||||
elementSources,
|
||||
readsFrom,
|
||||
resultSet,
|
||||
null,
|
||||
|
@ -372,10 +403,11 @@ class UpdatingQuery extends SqlQuery {
|
|||
this.fromContext,
|
||||
this.root,
|
||||
List<FoundElement> elements,
|
||||
List<SyntacticElementReference> elementSources,
|
||||
this.updates, {
|
||||
this.isInsert = false,
|
||||
this.resultSet,
|
||||
}) : super(name, elements);
|
||||
}) : super(name, elements, elementSources);
|
||||
}
|
||||
|
||||
/// A special kind of query running multiple inner queries in a transaction.
|
||||
|
@ -383,7 +415,11 @@ class InTransactionQuery extends SqlQuery {
|
|||
final List<SqlQuery> innerQueries;
|
||||
|
||||
InTransactionQuery(this.innerQueries, String name)
|
||||
: super(name, [for (final query in innerQueries) ...query.elements]);
|
||||
: super(
|
||||
name,
|
||||
[for (final query in innerQueries) ...query.elements],
|
||||
[for (final query in innerQueries) ...query.elementSources],
|
||||
);
|
||||
|
||||
@override
|
||||
InferredResultSet? get resultSet => null;
|
||||
|
@ -831,7 +867,7 @@ final class NestedResultQuery extends NestedResult {
|
|||
|
||||
/// Something in the query that needs special attention when generating code,
|
||||
/// such as variables or Dart placeholders.
|
||||
abstract class FoundElement {
|
||||
sealed class FoundElement {
|
||||
String get dartParameterName;
|
||||
|
||||
/// The name of this element as declared in the query
|
||||
|
@ -858,8 +894,27 @@ class FoundVariable extends FoundElement implements HasType {
|
|||
/// three [Variable]s in its AST, but only two [FoundVariable]s, where the
|
||||
/// `?` will have index 1 and (both) `:xyz` variables will have index 2. We
|
||||
/// only report one [FoundVariable] per index.
|
||||
///
|
||||
/// This [index] might change in the generator as variables are moved around.
|
||||
/// See [originalIndex] for the original index and a further discussion of
|
||||
/// this.
|
||||
int index;
|
||||
|
||||
/// The original index this variable had in the SQL string written by the
|
||||
/// user.
|
||||
///
|
||||
/// In the generator, we might have to shuffle variable indices around a bit
|
||||
/// to support array variables which occupy a dynamic amount of variable
|
||||
/// indices at runtime.
|
||||
/// For instance, consider `SELECT * FROM foo WHERE a = :a OR b IN :b OR c = :c`.
|
||||
/// Here, `:c` will have an original index of 3. Since `:b` is an array
|
||||
/// variable though, the actual query sent to the database system at runtime
|
||||
/// will look like `SELECT * FROM foo WHERE a = ?1 OR b IN (?3, ?4) OR c = ?2`
|
||||
/// when a size-2 list is passed for `b`. All non-array variables have been
|
||||
/// given indices that appear before the array to support this, so the [index]
|
||||
/// of `c` would then be `2`.
|
||||
final int originalIndex;
|
||||
|
||||
/// The name of this variable, or null if it's not a named variable.
|
||||
@override
|
||||
String? name;
|
||||
|
@ -875,10 +930,8 @@ class FoundVariable extends FoundElement implements HasType {
|
|||
@override
|
||||
final bool nullable;
|
||||
|
||||
/// The first [Variable] in the sql statement that has this [index].
|
||||
// todo: Do we really need to expose this? We only use [resolvedIndex], which
|
||||
// should always be equal to [index].
|
||||
final Variable variable;
|
||||
@override
|
||||
final AstNode syntacticOrigin;
|
||||
|
||||
/// Whether this variable is an array, which will be expanded into multiple
|
||||
/// variables at runtime. We only accept queries where no explicitly numbered
|
||||
|
@ -900,38 +953,38 @@ class FoundVariable extends FoundElement implements HasType {
|
|||
required this.index,
|
||||
required this.name,
|
||||
required this.sqlType,
|
||||
required this.variable,
|
||||
required Variable variable,
|
||||
this.nullable = false,
|
||||
this.isArray = false,
|
||||
this.isRequired = false,
|
||||
this.typeConverter,
|
||||
}) : hidden = false,
|
||||
forCaptured = null,
|
||||
assert(variable.resolvedIndex == index);
|
||||
}) : originalIndex = index,
|
||||
hidden = false,
|
||||
syntacticOrigin = variable,
|
||||
forCaptured = null;
|
||||
|
||||
FoundVariable.nestedQuery({
|
||||
required this.index,
|
||||
required this.name,
|
||||
required this.sqlType,
|
||||
required this.variable,
|
||||
required Variable variable,
|
||||
required this.forCaptured,
|
||||
}) : typeConverter = null,
|
||||
}) : originalIndex = index,
|
||||
typeConverter = null,
|
||||
nullable = false,
|
||||
isArray = false,
|
||||
isRequired = true,
|
||||
hidden = true;
|
||||
hidden = true,
|
||||
syntacticOrigin = variable;
|
||||
|
||||
@override
|
||||
String get dartParameterName {
|
||||
if (name != null) {
|
||||
return dartNameForSqlColumn(name!);
|
||||
} else {
|
||||
return 'var${variable.resolvedIndex}';
|
||||
return 'var$index';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AstNode get syntacticOrigin => variable;
|
||||
}
|
||||
|
||||
abstract class DartPlaceholderType {}
|
||||
|
|
|
@ -213,6 +213,7 @@ const _$SqlDialectEnumMap = {
|
|||
SqlDialect.sqlite: 'sqlite',
|
||||
SqlDialect.mysql: 'mysql',
|
||||
SqlDialect.postgres: 'postgres',
|
||||
SqlDialect.mariadb: 'mariadb',
|
||||
};
|
||||
|
||||
SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
|
||||
|
|
|
@ -755,9 +755,40 @@ class _ExpandedVariableWriter {
|
|||
_ExpandedVariableWriter(this.query, this._emitter);
|
||||
|
||||
void writeVariables() {
|
||||
_buffer.write('variables: [');
|
||||
_writeNewVariables();
|
||||
_buffer.write(']');
|
||||
_buffer.write('variables: ');
|
||||
|
||||
// Some dialects don't support variables with an explicit index. In that
|
||||
// case, we have to desugar them by duplicating variables, e.g. `:a AND :a`
|
||||
// would be transformed to `? AND ?` with us binding the value to both
|
||||
// variables.
|
||||
if (_emitter.writer.options.supportedDialects
|
||||
.any((e) => !e.supportsIndexedParameters) &&
|
||||
query.referencesAnyElementMoreThanOnce) {
|
||||
_buffer.write('executor.dialect.desugarDuplicateVariables([');
|
||||
_writeNewVariables();
|
||||
_buffer.write('],');
|
||||
|
||||
// Every time a variable is used in the generated SQL text, we have to
|
||||
// track the variable's index in the second list
|
||||
_buffer.write('[');
|
||||
for (final source in query.elementSources) {
|
||||
switch (source.referencedElement) {
|
||||
case FoundVariable variable:
|
||||
_buffer.write('${variable.index}, ');
|
||||
break;
|
||||
case FoundDartPlaceholder placeholder:
|
||||
final context = placeholderContextName(placeholder);
|
||||
_buffer.write('...$context.variableIndices');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_buffer.write('])');
|
||||
} else {
|
||||
_buffer.write('[');
|
||||
_writeNewVariables();
|
||||
_buffer.write(']');
|
||||
}
|
||||
}
|
||||
|
||||
void _writeNewVariables() {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:charcode/ascii.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show SqlDialect;
|
||||
// ignore: deprecated_member_use
|
||||
import 'package:drift/sqlite_keywords.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/utils/node_to_text.dart';
|
||||
|
||||
|
@ -87,25 +89,34 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
|
||||
@override
|
||||
bool isKeyword(String lexeme) {
|
||||
switch (dialect) {
|
||||
case SqlDialect.postgres:
|
||||
return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme);
|
||||
default:
|
||||
return isKeywordLexeme(lexeme);
|
||||
}
|
||||
return isKeywordLexeme(lexeme) ||
|
||||
switch (dialect) {
|
||||
SqlDialect.postgres => isPostgresKeywordLexeme(lexeme),
|
||||
SqlDialect.mariadb =>
|
||||
additionalMariaDBKeywords.contains(lexeme.toUpperCase()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
FoundVariable? _findMoorVar(Variable target) {
|
||||
return query!.variables.firstWhereOrNull(
|
||||
(f) => f.variable.resolvedIndex == target.resolvedIndex);
|
||||
@override
|
||||
String escapeIdentifier(String identifier) {
|
||||
return dialect.escape(identifier);
|
||||
}
|
||||
|
||||
void _writeMoorVariable(FoundVariable variable) {
|
||||
FoundVariable? _findVariable(Variable target) {
|
||||
return query!.variables
|
||||
.firstWhereOrNull((f) => f.originalIndex == target.resolvedIndex);
|
||||
}
|
||||
|
||||
void _writeAnalyzedVariable(FoundVariable variable) {
|
||||
if (variable.isArray) {
|
||||
_writeRawInSpaces('(\$${expandedName(variable)})');
|
||||
} else {
|
||||
final mark = _isPostgres ? '\\\$' : '?';
|
||||
_writeRawInSpaces('$mark${variable.index}');
|
||||
final syntax =
|
||||
dialect.supportsIndexedParameters ? '$mark${variable.index}' : mark;
|
||||
|
||||
_writeRawInSpaces(syntax);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,9 +175,9 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
|
||||
@override
|
||||
void visitNamedVariable(ColonNamedVariable e, void arg) {
|
||||
final moor = _findMoorVar(e);
|
||||
if (moor != null) {
|
||||
_writeMoorVariable(moor);
|
||||
final found = _findVariable(e);
|
||||
if (found != null) {
|
||||
_writeAnalyzedVariable(found);
|
||||
} else {
|
||||
super.visitNamedVariable(e, arg);
|
||||
}
|
||||
|
@ -174,9 +185,9 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
|
||||
@override
|
||||
void visitNumberedVariable(NumberedVariable e, void arg) {
|
||||
final moor = _findMoorVar(e);
|
||||
if (moor != null) {
|
||||
_writeMoorVariable(moor);
|
||||
final found = _findVariable(e);
|
||||
if (found != null) {
|
||||
_writeAnalyzedVariable(found);
|
||||
} else {
|
||||
super.visitNumberedVariable(e, arg);
|
||||
}
|
||||
|
@ -205,7 +216,12 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
}
|
||||
|
||||
final columnName = column.name;
|
||||
_out.write('"$table"."$columnName" AS "$prefix.$columnName"');
|
||||
|
||||
final escapedTable = escapeIdentifier(table);
|
||||
final escapedColumn = escapeIdentifier(columnName);
|
||||
final escapedAlias = escapeIdentifier('$prefix.$columnName');
|
||||
|
||||
_out.write('$escapedTable.$escapedColumn AS $escapedAlias');
|
||||
}
|
||||
} else if (e is DartPlaceholder) {
|
||||
final moorPlaceholder =
|
||||
|
|
|
@ -16,9 +16,19 @@ Map<SqlDialect, String> defaultConstraints(DriftColumn column) {
|
|||
for (final feature in column.constraints) {
|
||||
if (feature is PrimaryKeyColumn) {
|
||||
if (!wrotePkConstraint) {
|
||||
defaultConstraints.add(feature.isAutoIncrement
|
||||
? 'PRIMARY KEY AUTOINCREMENT'
|
||||
: 'PRIMARY KEY');
|
||||
if (feature.isAutoIncrement) {
|
||||
for (final dialect in SqlDialect.values) {
|
||||
if (dialect == SqlDialect.mariadb) {
|
||||
dialectSpecificConstraints[dialect]!
|
||||
.add('PRIMARY KEY AUTO_INCREMENT');
|
||||
} else {
|
||||
dialectSpecificConstraints[dialect]!
|
||||
.add('PRIMARY KEY AUTOINCREMENT');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
defaultConstraints.add('PRIMARY KEY');
|
||||
}
|
||||
|
||||
wrotePkConstraint = true;
|
||||
break;
|
||||
|
@ -63,9 +73,11 @@ Map<SqlDialect, String> defaultConstraints(DriftColumn column) {
|
|||
}
|
||||
|
||||
if (column.sqlType == DriftSqlType.bool) {
|
||||
final name = '"${column.nameInSql}"';
|
||||
final name = column.nameInSql;
|
||||
dialectSpecificConstraints[SqlDialect.sqlite]!
|
||||
.add('CHECK ($name IN (0, 1))');
|
||||
.add('CHECK (${SqlDialect.sqlite.escape(name)} IN (0, 1))');
|
||||
dialectSpecificConstraints[SqlDialect.mariadb]!
|
||||
.add('CHECK (${SqlDialect.mariadb.escape(name)} IN (0, 1))');
|
||||
}
|
||||
|
||||
for (final constraints in dialectSpecificConstraints.values) {
|
||||
|
|
|
@ -169,9 +169,8 @@ void main() {
|
|||
expect(generated, contains('.toList()'));
|
||||
});
|
||||
|
||||
group('generates correct code for expanded arrays', () {
|
||||
Future<void> runTest(DriftOptions options, Matcher expectation) async {
|
||||
final result = await generateForQueryInDriftFile('''
|
||||
test('generates correct code for expanded arrays', () async {
|
||||
final result = await generateForQueryInDriftFile('''
|
||||
CREATE TABLE tbl (
|
||||
a TEXT,
|
||||
b TEXT,
|
||||
|
@ -179,22 +178,17 @@ CREATE TABLE tbl (
|
|||
);
|
||||
|
||||
query: SELECT * FROM tbl WHERE a = :a AND b IN :b AND c = :c;
|
||||
''', options: options);
|
||||
expect(result, expectation);
|
||||
}
|
||||
|
||||
test('with the new query generator', () {
|
||||
return runTest(
|
||||
const DriftOptions.defaults(),
|
||||
allOf(
|
||||
contains(r'var $arrayStartIndex = 3;'),
|
||||
contains(r'SELECT * FROM tbl WHERE a = ?1 AND b IN ($expandedb) '
|
||||
'AND c = ?2'),
|
||||
contains(r'variables: [Variable<String>(a), Variable<String>(c), '
|
||||
r'for (var $ in b) Variable<String>($)], readsFrom: {tbl'),
|
||||
),
|
||||
);
|
||||
});
|
||||
''');
|
||||
expect(
|
||||
result,
|
||||
allOf(
|
||||
contains(r'var $arrayStartIndex = 3;'),
|
||||
contains(r'SELECT * FROM tbl WHERE a = ?1 AND b IN ($expandedb) '
|
||||
'AND c = ?2'),
|
||||
contains(r'variables: [Variable<String>(a), Variable<String>(c), '
|
||||
r'for (var $ in b) Variable<String>($)], readsFrom: {tbl'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
|
|
|
@ -14,7 +14,7 @@ void main() {
|
|||
}) {
|
||||
final engine = SqlEngine();
|
||||
final context = engine.analyze(sql);
|
||||
final query = SqlSelectQuery('name', context, context.root, [], [],
|
||||
final query = SqlSelectQuery('name', context, context.root, [], [], [],
|
||||
InferredResultSet(null, []), null, null);
|
||||
|
||||
final result = SqlWriter(options, dialect: dialect, query: query).write();
|
||||
|
|
|
@ -831,8 +831,10 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
|
|||
@override
|
||||
String get entityName => 'group_count';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users',
|
||||
};
|
||||
@override
|
||||
GroupCount get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||
import 'package:drift/drift.dart' as i1;
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
final class _S2 extends i0.VersionedSchema {
|
||||
|
|
|
@ -490,8 +490,10 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
|
|||
@override
|
||||
String get entityName => 'group_count';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'
|
||||
};
|
||||
@override
|
||||
GroupCount get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -531,8 +531,10 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
|
|||
@override
|
||||
String get entityName => 'group_count';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'
|
||||
};
|
||||
@override
|
||||
GroupCount get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -531,8 +531,10 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
|
|||
@override
|
||||
String get entityName => 'group_count';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'
|
||||
};
|
||||
@override
|
||||
GroupCount get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -535,8 +535,10 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
|
|||
@override
|
||||
String get entityName => 'group_count';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'
|
||||
};
|
||||
@override
|
||||
GroupCount get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -735,8 +735,10 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
|
|||
@override
|
||||
String get entityName => 'group_count';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW group_count AS SELECT\n users.*,\n (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count\n FROM users;';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW group_count AS SELECT\n users.*,\n (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count\n FROM users;'
|
||||
};
|
||||
@override
|
||||
GroupCount get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
# Remove the following pattern if you wish to check in your lock file
|
||||
pubspec.lock
|
||||
|
||||
# Conventional directory for build outputs
|
||||
build/
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
|
@ -0,0 +1,29 @@
|
|||
An experimental mariadb backend for Drift.
|
||||
|
||||
Note that the backend is currently experimental and not fully functional yet.
|
||||
|
||||
## Using this
|
||||
|
||||
For general notes on using drift, see [this guide](https://drift.simonbinder.eu/getting-started/).
|
||||
|
||||
To use drift_mariadb, add this to your `pubspec.yaml`
|
||||
```yaml
|
||||
dependencies:
|
||||
drift: "$latest version"
|
||||
drift_mariadb:
|
||||
git:
|
||||
url: https://github.com/simolus3/drift.git
|
||||
path: extras/drift_mariadb
|
||||
```
|
||||
|
||||
To connect your drift database class to mariadb, use a `MariaDBDatabase` from `package:drift_mariadb/mariadb.dart`.
|
||||
|
||||
## Testing
|
||||
|
||||
To test this package, first run
|
||||
|
||||
```
|
||||
docker run -p 3306:3306 -e MARIADB_ROOT_PASSWORD=password -e MARIADB_DATABASE=database mariadb:latest
|
||||
```
|
||||
|
||||
It can then be tested with `dart test`.
|
|
@ -0,0 +1,5 @@
|
|||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
language:
|
||||
strict-casts: true
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:drift/backends.dart';
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart';
|
||||
import 'package:drift_mariadb/drift_mariadb.dart';
|
||||
import 'package:mysql_client/mysql_client.dart';
|
||||
|
||||
void main() async {
|
||||
final mariadb = MariaDBDatabase(
|
||||
pool: MySQLConnectionPool(
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
userName: 'root',
|
||||
password: 'password',
|
||||
databaseName: 'database',
|
||||
maxConnections: 1,
|
||||
secure: false,
|
||||
),
|
||||
logStatements: true,
|
||||
);
|
||||
|
||||
await mariadb.ensureOpen(_NullUser());
|
||||
|
||||
final rows = await mariadb.runSelect(r'SELECT (?)', [true]);
|
||||
final row = rows.single;
|
||||
print(row);
|
||||
print(row.values.map((e) => e.runtimeType).toList());
|
||||
}
|
||||
|
||||
class _NullUser extends QueryExecutorUser {
|
||||
@override
|
||||
Future<void> beforeOpen(
|
||||
QueryExecutor executor,
|
||||
OpeningDetails details,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/// Experimental Drift integration for MariaDB.
|
||||
@experimental
|
||||
library drift.mariadb;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
export 'src/mariadb_database.dart';
|
|
@ -0,0 +1,225 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/backends.dart';
|
||||
import 'package:mysql_client/mysql_client.dart';
|
||||
|
||||
/// A drift database implementation that talks to a mariadb database.
|
||||
class MariaDBDatabase extends DelegatedDatabase {
|
||||
MariaDBDatabase({
|
||||
required MySQLConnectionPool pool,
|
||||
bool isSequential = true,
|
||||
bool logStatements = false,
|
||||
}) : super(
|
||||
_MariaDelegate(() => pool, true),
|
||||
isSequential: isSequential,
|
||||
logStatements: logStatements,
|
||||
);
|
||||
|
||||
/// Creates a drift database implementation from a mariadb database
|
||||
/// [connection].
|
||||
MariaDBDatabase.opened(
|
||||
MySQLConnectionPool connection, {
|
||||
bool logStatements = false,
|
||||
}) : super(
|
||||
_MariaDelegate(() => connection, false),
|
||||
isSequential: true,
|
||||
logStatements: logStatements,
|
||||
);
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => SqlDialect.mariadb;
|
||||
}
|
||||
|
||||
class _MariaDelegate extends DatabaseDelegate {
|
||||
_MariaDelegate(this._open, this.closeUnderlyingWhenClosed);
|
||||
|
||||
final bool closeUnderlyingWhenClosed;
|
||||
final FutureOr<MySQLConnectionPool> Function() _open;
|
||||
|
||||
MySQLConnectionPool? _openedSession;
|
||||
|
||||
@override
|
||||
TransactionDelegate get transactionDelegate => NoTransactionDelegate(
|
||||
start: 'START TRANSACTION',
|
||||
commit: 'COMMIT',
|
||||
rollback: 'ROLLBACK',
|
||||
savepoint: (int depth) => 'SAVEPOINT s$depth',
|
||||
release: (int depth) => 'RELEASE SAVEPOINT s$depth',
|
||||
rollbackToSavepoint: (int depth) => 'ROLLBACK TO SAVEPOINT s$depth',
|
||||
);
|
||||
|
||||
@override
|
||||
late DbVersionDelegate versionDelegate;
|
||||
|
||||
@override
|
||||
Future<bool> get isOpen => Future.value(_openedSession != null);
|
||||
|
||||
@override
|
||||
Future<void> open(QueryExecutorUser user) async {
|
||||
final session = await _open();
|
||||
final mariaVersionDelegate = _MariaVersionDelegate(session);
|
||||
|
||||
await mariaVersionDelegate.init();
|
||||
|
||||
_openedSession = session;
|
||||
versionDelegate = mariaVersionDelegate;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) async {
|
||||
final session = _openedSession!;
|
||||
final prepared = List<PreparedStmt?>.filled(
|
||||
statements.statements.length,
|
||||
null,
|
||||
);
|
||||
|
||||
try {
|
||||
for (final instantiation in statements.arguments) {
|
||||
final mariaArgs = _BoundArguments.ofDartArgs(instantiation.arguments);
|
||||
final stmtIndex = instantiation.statementIndex;
|
||||
var stmt = prepared[stmtIndex];
|
||||
final sql = statements.statements[stmtIndex];
|
||||
stmt ??= prepared[stmtIndex] = await session.prepare(sql);
|
||||
await stmt.execute(mariaArgs.parameters);
|
||||
}
|
||||
} finally {
|
||||
for (var stmt in prepared) {
|
||||
stmt?.deallocate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _runWithArgs(String statement, List<Object?> args) async {
|
||||
final session = _openedSession!;
|
||||
|
||||
IResultSet result;
|
||||
if (args.isEmpty) {
|
||||
result = await session.execute(statement);
|
||||
} else {
|
||||
var mariaArgs = _BoundArguments.ofDartArgs(args);
|
||||
var stmt = await session.prepare(statement);
|
||||
result = await stmt.execute(mariaArgs.parameters);
|
||||
}
|
||||
return result.affectedRows.toInt();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, List<Object?> args) async {
|
||||
await _runWithArgs(statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) async {
|
||||
final session = _openedSession!;
|
||||
|
||||
IResultSet result;
|
||||
if (args.isEmpty) {
|
||||
result = await session.execute(statement);
|
||||
} else {
|
||||
var mariaArgs = _BoundArguments.ofDartArgs(args);
|
||||
var stmt = await session.prepare(statement);
|
||||
|
||||
result = await stmt.execute(mariaArgs.parameters);
|
||||
}
|
||||
|
||||
return result.firstOrNull?.lastInsertID.toInt() ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List<Object?> args) async {
|
||||
return _runWithArgs(statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
||||
var session = _openedSession!;
|
||||
IResultSet result;
|
||||
if (args.isEmpty) {
|
||||
result = await session.execute(statement);
|
||||
} else {
|
||||
var mariaArgs = _BoundArguments.ofDartArgs(args);
|
||||
var stmt = await session.prepare(statement);
|
||||
result = await stmt.execute(mariaArgs.parameters);
|
||||
}
|
||||
print(statement);
|
||||
var rowsList = result.rows.toList();
|
||||
var rowsParsed = List.generate(
|
||||
rowsList.length,
|
||||
(index) => rowsList[index].typedAssoc().values.toList(),
|
||||
);
|
||||
|
||||
return QueryResult(
|
||||
[for (final mariaColumn in result.cols) mariaColumn.name],
|
||||
rowsParsed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (closeUnderlyingWhenClosed) {
|
||||
await _openedSession?.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BoundArguments {
|
||||
final List<Object?> parameters;
|
||||
|
||||
_BoundArguments(this.parameters);
|
||||
|
||||
factory _BoundArguments.ofDartArgs(List<Object?> args) {
|
||||
final parameters = <Object?>[];
|
||||
|
||||
void add(Object? param) {
|
||||
parameters.add(param);
|
||||
}
|
||||
|
||||
for (final value in args) {
|
||||
if (value == null) {
|
||||
add(null);
|
||||
} else if (value is int) {
|
||||
add(value);
|
||||
} else if (value is BigInt) {
|
||||
add(value);
|
||||
} else if (value is bool) {
|
||||
add(value ? 1 : 0);
|
||||
} else if (value is double) {
|
||||
add(value);
|
||||
} else if (value is String) {
|
||||
add(value);
|
||||
} else {
|
||||
throw ArgumentError.value(value, 'value', 'Unsupported type');
|
||||
}
|
||||
}
|
||||
|
||||
return _BoundArguments(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
class _MariaVersionDelegate extends DynamicVersionDelegate {
|
||||
final MySQLConnectionPool database;
|
||||
|
||||
_MariaVersionDelegate(this.database);
|
||||
|
||||
@override
|
||||
Future<int> get schemaVersion async {
|
||||
final result = await database.execute('SELECT version FROM __schema');
|
||||
return result.rows.first.typedAssoc()['version'] as int;
|
||||
}
|
||||
|
||||
Future init() async {
|
||||
await database.execute('CREATE TABLE IF NOT EXISTS __schema (version '
|
||||
'integer NOT NULL DEFAULT 0)');
|
||||
|
||||
final count = await database.execute('SELECT COUNT(*) FROM __schema');
|
||||
if (count.rows.first.typedAssoc()['COUNT(*)'] as int == 0) {
|
||||
await database.execute('INSERT INTO __schema (version) VALUES (0)');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setSchemaVersion(int version) async {
|
||||
var stmt = await database.prepare(r'UPDATE __schema SET version = (?)');
|
||||
await stmt.execute([version]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
name: drift_mariadb
|
||||
description: Mariadb support for drift.
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
drift: ^2.0.0
|
||||
meta: ^1.9.1
|
||||
mysql_client: ^0.0.27
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.21.0
|
||||
drift_testcases:
|
||||
path: ../integration_tests/drift_testcases
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:drift_mariadb/drift_mariadb.dart';
|
||||
import 'package:drift_testcases/tests.dart';
|
||||
import 'package:mysql_client/mysql_client.dart';
|
||||
|
||||
class MariaDbExecutor extends TestExecutor {
|
||||
@override
|
||||
bool get supportsReturning => true;
|
||||
|
||||
@override
|
||||
bool get supportsNestedTransactions => true;
|
||||
|
||||
@override
|
||||
DatabaseConnection createConnection() {
|
||||
final pool = MySQLConnectionPool(
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
userName: 'root',
|
||||
password: 'password',
|
||||
databaseName: 'database',
|
||||
maxConnections: 1,
|
||||
secure: false,
|
||||
);
|
||||
|
||||
return DatabaseConnection(MariaDBDatabase(pool: pool, logStatements: true));
|
||||
}
|
||||
|
||||
@override
|
||||
Future clearDatabaseAndClose(Database db) async {
|
||||
await db.customStatement('DROP DATABASE `database`;');
|
||||
await db.customStatement('CREATE DATABASE `database`;');
|
||||
|
||||
await db.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteData() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runAllTests(MariaDbExecutor());
|
||||
}
|
|
@ -3,7 +3,7 @@ description: Postgres support for drift
|
|||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0-0 <3.0.0'
|
||||
sdk: '>=2.12.0-0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
collection: ^1.16.0
|
||||
|
|
|
@ -9,7 +9,12 @@ targets:
|
|||
raw_result_set_data: false
|
||||
named_parameters: false
|
||||
sql:
|
||||
dialects: [sqlite, postgres]
|
||||
# Generate multi-dialect code so that the tests can run on each DBMS we're
|
||||
# working on (even though only sqlite is supported officially).
|
||||
dialects:
|
||||
- sqlite
|
||||
- postgres
|
||||
- mariadb
|
||||
options:
|
||||
version: "3.37"
|
||||
modules:
|
||||
|
|
|
@ -10,13 +10,16 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
$UsersTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumn<int> id =
|
||||
GeneratedColumn<int>('id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
||||
SqlDialect.sqlite: 'PRIMARY KEY AUTOINCREMENT',
|
||||
SqlDialect.postgres: 'PRIMARY KEY AUTOINCREMENT',
|
||||
SqlDialect.mariadb: 'PRIMARY KEY AUTO_INCREMENT',
|
||||
}));
|
||||
static const VerificationMeta _nameMeta = const VerificationMeta('name');
|
||||
@override
|
||||
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||
|
@ -337,6 +340,7 @@ class $FriendshipsTable extends Friendships
|
|||
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
||||
SqlDialect.sqlite: 'CHECK ("really_good_friends" IN (0, 1))',
|
||||
SqlDialect.postgres: '',
|
||||
SqlDialect.mariadb: 'CHECK (`really_good_friends` IN (0, 1))',
|
||||
}),
|
||||
defaultValue: const Constant(false));
|
||||
@override
|
||||
|
@ -556,9 +560,11 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
switch (executor.dialect) {
|
||||
SqlDialect.sqlite =>
|
||||
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT ?1',
|
||||
SqlDialect.postgres ||
|
||||
_ =>
|
||||
SqlDialect.postgres =>
|
||||
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT \$1',
|
||||
SqlDialect.mariadb ||
|
||||
_ =>
|
||||
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT ?',
|
||||
},
|
||||
variables: [
|
||||
Variable<int>(amount)
|
||||
|
@ -574,13 +580,18 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
switch (executor.dialect) {
|
||||
SqlDialect.sqlite =>
|
||||
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = ?1 OR f.second_user = ?1)',
|
||||
SqlDialect.postgres ||
|
||||
_ =>
|
||||
SqlDialect.postgres =>
|
||||
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = \$1 OR f.second_user = \$1)',
|
||||
SqlDialect.mariadb ||
|
||||
_ =>
|
||||
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = ? OR f.second_user = ?)',
|
||||
},
|
||||
variables: [
|
||||
variables: executor.dialect.desugarDuplicateVariables([
|
||||
Variable<int>(user)
|
||||
],
|
||||
], [
|
||||
1,
|
||||
1,
|
||||
]),
|
||||
readsFrom: {
|
||||
friendships,
|
||||
}).map((QueryRow row) => row.read<int>('_c0'));
|
||||
|
@ -591,13 +602,19 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
switch (executor.dialect) {
|
||||
SqlDialect.sqlite =>
|
||||
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS user ON user.id IN (f.first_user, f.second_user) AND user.id != ?1 WHERE(f.first_user = ?1 OR f.second_user = ?1)',
|
||||
SqlDialect.postgres ||
|
||||
_ =>
|
||||
SqlDialect.postgres =>
|
||||
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS "user" ON "user".id IN (f.first_user, f.second_user) AND "user".id != \$1 WHERE(f.first_user = \$1 OR f.second_user = \$1)',
|
||||
SqlDialect.mariadb ||
|
||||
_ =>
|
||||
'SELECT f.really_good_friends,`user`.`id` AS `nested_0.id`, `user`.`name` AS `nested_0.name`, `user`.`birth_date` AS `nested_0.birth_date`, `user`.`profile_picture` AS `nested_0.profile_picture`, `user`.`preferences` AS `nested_0.preferences` FROM friendships AS f INNER JOIN users AS user ON user.id IN (f.first_user, f.second_user) AND user.id != ? WHERE(f.first_user = ? OR f.second_user = ?)',
|
||||
},
|
||||
variables: [
|
||||
variables: executor.dialect.desugarDuplicateVariables([
|
||||
Variable<int>(user)
|
||||
],
|
||||
], [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
]),
|
||||
readsFrom: {
|
||||
friendships,
|
||||
users,
|
||||
|
@ -619,9 +636,10 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
return customSelect(
|
||||
switch (executor.dialect) {
|
||||
SqlDialect.sqlite => 'SELECT preferences FROM users WHERE id = ?1',
|
||||
SqlDialect.postgres ||
|
||||
SqlDialect.postgres => 'SELECT preferences FROM users WHERE id = \$1',
|
||||
SqlDialect.mariadb ||
|
||||
_ =>
|
||||
'SELECT preferences FROM users WHERE id = \$1',
|
||||
'SELECT preferences FROM users WHERE id = ?',
|
||||
},
|
||||
variables: [
|
||||
Variable<int>(user)
|
||||
|
@ -650,9 +668,11 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
switch (executor.dialect) {
|
||||
SqlDialect.sqlite =>
|
||||
'INSERT INTO friendships VALUES (?1, ?2, ?3) RETURNING *',
|
||||
SqlDialect.postgres ||
|
||||
_ =>
|
||||
SqlDialect.postgres =>
|
||||
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *',
|
||||
SqlDialect.mariadb ||
|
||||
_ =>
|
||||
'INSERT INTO friendships VALUES (?, ?, ?) RETURNING *',
|
||||
},
|
||||
variables: [
|
||||
Variable<int>(var1),
|
||||
|
|
|
@ -33,9 +33,16 @@ void crudTests(TestExecutor executor) {
|
|||
final db = Database(executor.createConnection());
|
||||
|
||||
await expectLater(
|
||||
db.into(db.users).insert(marcell),
|
||||
throwsA(toString(
|
||||
matches(RegExp(r'unique constraint', caseSensitive: false)))));
|
||||
db.into(db.users).insert(marcell),
|
||||
throwsA(
|
||||
toString(anyOf(
|
||||
// sqlite3 and postgres
|
||||
matches(RegExp(r'unique constraint', caseSensitive: false)),
|
||||
// mariadb
|
||||
matches(RegExp(r'duplicate entry', caseSensitive: false)),
|
||||
)),
|
||||
),
|
||||
);
|
||||
await executor.clearDatabaseAndClose(db);
|
||||
});
|
||||
|
||||
|
@ -98,15 +105,16 @@ void crudTests(TestExecutor executor) {
|
|||
final db = Database(executor.createConnection());
|
||||
|
||||
// ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
|
||||
if (db.executor.dialect == SqlDialect.postgres) {
|
||||
await db.customStatement(
|
||||
'INSERT INTO friendships (first_user, second_user) VALUES (@1, @2)',
|
||||
<int>[1, 2]);
|
||||
} else {
|
||||
await db.customStatement(
|
||||
'INSERT INTO friendships (first_user, second_user) VALUES (?1, ?2)',
|
||||
<int>[1, 2]);
|
||||
}
|
||||
await db.customStatement(
|
||||
switch (db.executor.dialect) {
|
||||
SqlDialect.postgres =>
|
||||
r'INSERT INTO friendships (first_user, second_user) VALUES ($1, $2)',
|
||||
SqlDialect.mariadb =>
|
||||
r'INSERT INTO friendships (first_user, second_user) VALUES (?, ?)',
|
||||
_ =>
|
||||
r'INSERT INTO friendships (first_user, second_user) VALUES (?1, ?2)',
|
||||
},
|
||||
<int>[1, 2]);
|
||||
|
||||
expect(await db.friendsOf(1).get(), isNotEmpty);
|
||||
await executor.clearDatabaseAndClose(db);
|
||||
|
@ -120,7 +128,8 @@ void crudTests(TestExecutor executor) {
|
|||
|
||||
Future<T?> evaluate<T extends Object>(Expression<T> expr) async {
|
||||
late final Expression<T> effectiveExpr;
|
||||
if (database.executor.dialect == SqlDialect.postgres) {
|
||||
final dialect = database.executor.dialect;
|
||||
if (dialect == SqlDialect.postgres || dialect == SqlDialect.mariadb) {
|
||||
// 'SELECT'ing values that don't come from a table return as String
|
||||
// by default, so we need to explicitly cast it to the expected type
|
||||
// https://www.postgresql.org/docs/current/typeconv-select.html
|
||||
|
|
|
@ -9,6 +9,7 @@ packages:
|
|||
- sqlparser
|
||||
- examples/*
|
||||
- extras/benchmarks
|
||||
- extras/drift_mariadb
|
||||
- extras/drift_postgres
|
||||
- extras/encryption
|
||||
- extras/integration_tests/*
|
||||
|
|
|
@ -28,6 +28,10 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
return isKeywordLexeme(lexeme);
|
||||
}
|
||||
|
||||
String escapeIdentifier(String identifier) {
|
||||
return '"$identifier"';
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAggregateFunctionInvocation(
|
||||
AggregateFunctionInvocation e, void arg) {
|
||||
|
@ -1343,7 +1347,7 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
void identifier(String identifier,
|
||||
{bool spaceBefore = true, bool spaceAfter = true}) {
|
||||
if (isKeyword(identifier) || _notAKeywordRegex.hasMatch(identifier)) {
|
||||
identifier = '"$identifier"';
|
||||
identifier = escapeIdentifier(identifier);
|
||||
}
|
||||
|
||||
symbol(identifier, spaceBefore: spaceBefore, spaceAfter: spaceAfter);
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
pushd extras/drift_postgres
|
||||
echo "Running integration tests with Postgres"
|
||||
dart pub upgrade
|
||||
dart test
|
||||
popd
|
||||
|
||||
pushd examples/with_built_value
|
||||
echo "Running build runner in with_built_value"
|
||||
dart pub upgrade
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
popd
|
||||
|
||||
pushd examples/modular
|
||||
echo "Running build runner in modular example"
|
||||
dart pub upgrade
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
dart run bin/example.dart
|
||||
popd
|
||||
|
||||
pushd examples/migrations_example
|
||||
echo "Testing migrations in migrations_example"
|
||||
dart pub upgrade
|
||||
dart test
|
||||
popd
|
Loading…
Reference in New Issue