diff --git a/drift/lib/src/runtime/query_builder/expressions/variables.dart b/drift/lib/src/runtime/query_builder/expressions/variables.dart index 3d66658e5..4243a5e48 100644 --- a/drift/lib/src/runtime/query_builder/expressions/variables.dart +++ b/drift/lib/src/runtime/query_builder/expressions/variables.dart @@ -84,17 +84,15 @@ final class Variable extends Expression { var explicitStart = context.explicitVariableIndex; var mark = '?'; - var suffix = ''; if (context.dialect == SqlDialect.postgres) { explicitStart = 1; mark = r'$'; } - if (explicitStart != null) { + if (explicitStart != null && context.dialect.supportsIndexedParameters) { context.buffer ..write(mark) - ..write(explicitStart + context.amountOfVariables) - ..write(suffix); + ..write(explicitStart + context.amountOfVariables); context.introduceVariable(this, mapToSimpleValue(context)); } else { context.buffer.write(mark); diff --git a/drift/lib/src/runtime/query_builder/query_builder.dart b/drift/lib/src/runtime/query_builder/query_builder.dart index 879eef5a9..1c20851c3 100644 --- a/drift/lib/src/runtime/query_builder/query_builder.dart +++ b/drift/lib/src/runtime/query_builder/query_builder.dart @@ -129,6 +129,16 @@ enum SqlDialect { realType: 'float8', ), + /// DuckDB (currently supported in an experimental state) + duckdb( + booleanType: 'BOOLEAN', + textType: 'TEXT', + integerType: 'BIGINT', + blobType: 'BLOB', + realType: 'DOUBLE', + supportsIndexedParameters: false, + ), + /// MariaDB (currently supported in an experimental state) mariadb( booleanType: 'BOOLEAN', diff --git a/drift/lib/src/runtime/query_builder/statements/insert.dart b/drift/lib/src/runtime/query_builder/statements/insert.dart index ad110c069..1fe066794 100644 --- a/drift/lib/src/runtime/query_builder/statements/insert.dart +++ b/drift/lib/src/runtime/query_builder/statements/insert.dart @@ -511,11 +511,22 @@ enum InsertMode implements Component { throw ArgumentError('$this not supported on postgres'); } - ctx.buffer.write( - _insertKeywords[ctx.dialect == SqlDialect.postgres - ? InsertMode.insert - : this], - ); + if (ctx.dialect == SqlDialect.duckdb && + this != InsertMode.insert && + this != InsertMode.replace && + this != InsertMode.insertOrReplace && + this != InsertMode.insertOrIgnore) { + throw ArgumentError('$this not supported on duckdb'); + } + + final effectiveMode = switch (ctx.dialect) { + SqlDialect.postgres => InsertMode.insert, + SqlDialect.duckdb when this == InsertMode.replace => + InsertMode.insertOrReplace, + _ => this, + }; + + ctx.buffer.write(_insertKeywords[effectiveMode]); } } diff --git a/drift/test/database/duckdb_dialect_test.dart b/drift/test/database/duckdb_dialect_test.dart new file mode 100644 index 000000000..60cb2ba71 --- /dev/null +++ b/drift/test/database/duckdb_dialect_test.dart @@ -0,0 +1,179 @@ +import 'package:drift/drift.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import '../generated/todos.dart'; +import '../test_utils/test_utils.dart'; + +void main() { + String writeColumn(GeneratedColumn column) { + final context = stubContext(dialect: SqlDialect.duckdb); + column.writeColumnDefinition(context); + return context.sql; + } + + group('schema generation', () { + test('writes BIGINT for integer columns', () { + expect( + writeColumn( + GeneratedColumn('value', 'tbl', false, type: DriftSqlType.int), + ), + '"value" BIGINT NOT NULL', + ); + }); + + test('writes BIGINT for int64 columns', () { + expect( + writeColumn( + GeneratedColumn( + 'value', + 'tbl', + false, + type: DriftSqlType.bigInt, + ), + ), + '"value" BIGINT NOT NULL', + ); + }); + + test('writes BOOLEAN for boolean columns', () { + expect( + writeColumn( + GeneratedColumn('value', 'tbl', false, type: DriftSqlType.bool), + ), + '"value" BOOLEAN NOT NULL', + ); + }); + + test('writes DOUBLE for real columns', () { + expect( + writeColumn( + GeneratedColumn( + 'value', + 'tbl', + false, + type: DriftSqlType.double, + ), + ), + '"value" DOUBLE NOT NULL', + ); + }); + }); + + group('variables', () { + test('use question mark placeholders', () { + expect( + const Variable(1), + generatesWithOptions('?', variables: [1], dialect: SqlDialect.duckdb), + ); + }); + + test('do not generate sqlite-style indexed placeholders', () { + final context = stubContext(dialect: SqlDialect.duckdb) + ..explicitVariableIndex = 3; + + const Variable(1).writeInto(context); + + expect(context.sql, '?'); + expect(context.boundVariables, [1]); + }); + }); + + group('casts', () { + test('cast() uses BIGINT', () { + expect( + const Variable(1).cast(), + generatesWithOptions( + 'CAST(? AS BIGINT)', + variables: [1], + dialect: SqlDialect.duckdb, + ), + ); + }); + + test('cast() uses BIGINT', () { + expect( + Variable(BigInt.one).cast(), + generatesWithOptions( + 'CAST(? AS BIGINT)', + variables: [BigInt.one], + dialect: SqlDialect.duckdb, + ), + ); + }); + }); + + group('query generation', () { + late TodoDb db; + late MockExecutor executor; + + setUp(() { + executor = MockExecutor(); + when(executor.dialect).thenReturn(SqlDialect.duckdb); + db = TodoDb(executor); + }); + + test('inserts with question mark placeholders', () async { + await db + .into(db.tableWithoutPK) + .insert( + CustomRowClass.map( + 42, + 3.1415, + webSafeInt: BigInt.one, + custom: MyCustomObject('custom'), + ).toInsertable(), + ); + + verify( + executor.runInsert( + 'INSERT INTO "table_without_p_k" ' + '("not_really_an_id", "some_float", "web_safe_int", "custom") ' + 'VALUES (?, ?, ?, ?)', + [42, 3.1415, BigInt.one, anything], + ), + ); + }); + + test('selects with question mark placeholders', () async { + when(executor.runSelect(any, any)).thenAnswer((_) async => []); + + await (db.select( + db.tableWithoutPK, + )..where((t) => t.notReallyAnId.equals(42))).get(); + + verify( + executor.runSelect( + 'SELECT * FROM "table_without_p_k" WHERE "not_really_an_id" = ?;', + [42], + ), + ); + }); + + test('supports RETURNING * for inserts', () async { + when(executor.runSelect(any, any)).thenAnswer( + (_) async => [ + { + 'id': 1, + 'desc': 'description', + 'description_in_upper_case': 'DESCRIPTION', + 'priority': 1, + }, + ], + ); + + await db + .into(db.categories) + .insertReturning( + CategoriesCompanion.insert(description: 'description'), + ); + + verify( + executor.runSelect( + 'INSERT INTO "categories" ("desc") VALUES (?) RETURNING *', + ['description'], + ), + ); + }); + }); +} diff --git a/drift_dev/lib/src/generated/analysis/options.g.dart b/drift_dev/lib/src/generated/analysis/options.g.dart index 25efa45c6..61e2bd57c 100644 --- a/drift_dev/lib/src/generated/analysis/options.g.dart +++ b/drift_dev/lib/src/generated/analysis/options.g.dart @@ -309,6 +309,7 @@ const _$SqlDialectEnumMap = { SqlDialect.sqlite: 'sqlite', SqlDialect.mysql: 'mysql', SqlDialect.postgres: 'postgres', + SqlDialect.duckdb: 'duckdb', SqlDialect.mariadb: 'mariadb', }; diff --git a/drift_dev/lib/src/writer/utils/column_constraints.dart b/drift_dev/lib/src/writer/utils/column_constraints.dart index 3dd53b37f..4d42504a7 100644 --- a/drift_dev/lib/src/writer/utils/column_constraints.dart +++ b/drift_dev/lib/src/writer/utils/column_constraints.dart @@ -24,6 +24,8 @@ Map defaultConstraints(DriftColumn column) { dialectSpecificConstraints[dialect]!.add( 'PRIMARY KEY AUTO_INCREMENT', ); + } else if (dialect == SqlDialect.duckdb) { + dialectSpecificConstraints[dialect]!.add('PRIMARY KEY'); } else { dialectSpecificConstraints[dialect]!.add( 'PRIMARY KEY AUTOINCREMENT',