diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index e4e7ef3b..90151da5 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -4,6 +4,7 @@ generated companion class. - Add the `TypeConverter.extensionType` factory to create type converters for extension types. +- Fix invalid SQL syntax being generated for `BLOB` literals on postgres. ## 2.16.0 diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart index 26b1211d..e6081278 100644 --- a/drift/lib/src/runtime/types/mapping.dart +++ b/drift/lib/src/runtime/types/mapping.dart @@ -115,9 +115,17 @@ final class SqlTypes { return (dart.millisecondsSinceEpoch ~/ 1000).toString(); } } else if (dart is Uint8List) { - // BLOB literals are string literals containing hexadecimal data and - // preceded by a single "x" or "X" character. Example: X'53514C697465' - return "x'${hex.encode(dart)}'"; + final String hexString = hex.encode(dart); + + if (dialect == SqlDialect.postgres) { + // Postgres BYTEA hex format + // https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-HEX-FORMAT + return "'\\x$hexString'::bytea"; + } else { + // BLOB literals are string literals containing hexadecimal data and + // preceded by a single "x" or "X" character. Example: X'53514C697465' + return "x'$hexString'"; + } } else if (dart is DriftAny) { return mapToSqlLiteral(dart.rawSqlValue); } diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index bbcfdcc0..8d6b0e3b 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -2,6 +2,9 @@ - Fix drift using the wrong import alias in generated part files. - Add the `use_sql_column_name_as_json_key` builder option. +- Add a `setup` parameter to `SchemaVerifier`. It is called when the verifier + creates database connections (similar to the callback on `NativeDatabase`) + and can be used to register custom functions. ## 2.16.0 diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index a09cdf15..73047c8b 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -11,8 +11,18 @@ export 'package:drift_dev/src/services/schema/verifier_common.dart' show SchemaMismatch; abstract class SchemaVerifier { - factory SchemaVerifier(SchemaInstantiationHelper helper) = - VerifierImplementation; + /// Creates a schema verifier for the drift-generated [helper]. + /// + /// See [tests] for more information. + /// The optional [setup] parameter is used internally by the verifier for + /// every database connection it opens. This can be used to, for instance, + /// register custom functions expected by your database. + /// + /// [tests]: https://drift.simonbinder.eu/docs/migrations/tests/ + factory SchemaVerifier( + SchemaInstantiationHelper helper, { + void Function(Database raw)? setup, + }) = VerifierImplementation; /// Creates a [DatabaseConnection] that contains empty tables created for the /// known schema [version]. diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart index ba8b4941..97e54a38 100644 --- a/drift_dev/lib/src/services/schema/verifier_impl.dart +++ b/drift_dev/lib/src/services/schema/verifier_impl.dart @@ -13,8 +13,9 @@ Expando> expectedSchema = Expando(); class VerifierImplementation implements SchemaVerifier { final SchemaInstantiationHelper helper; final Random _random = Random(); + final void Function(Database)? setup; - VerifierImplementation(this.helper); + VerifierImplementation(this.helper, {this.setup}); @override Future migrateAndValidate(GeneratedDatabase db, int expectedVersion, @@ -57,14 +58,20 @@ class VerifierImplementation implements SchemaVerifier { return buffer.toString(); } + Database _setupDatabase(String uri) { + final database = sqlite3.open(uri, uri: true); + setup?.call(database); + return database; + } + @override Future schemaAt(int version) async { // Use distinct executors for setup and use, allowing us to close the helper // db here and avoid creating it twice. // https://www.sqlite.org/inmemorydb.html#sharedmemdb final uri = 'file:mem${_randomString()}?mode=memory&cache=shared'; - final dbForSetup = sqlite3.open(uri, uri: true); - final dbForUse = sqlite3.open(uri, uri: true); + final dbForSetup = _setupDatabase(uri); + final dbForUse = _setupDatabase(uri); final executor = NativeDatabase.opened(dbForSetup); final db = helper.databaseForVersion(executor, version); @@ -74,7 +81,7 @@ class VerifierImplementation implements SchemaVerifier { await db.close(); return InitializedSchema(dbForUse, () { - final db = sqlite3.open(uri, uri: true); + final db = _setupDatabase(uri); return DatabaseConnection(NativeDatabase.opened(db)); }); } diff --git a/drift_dev/test/services/schema/verifier_impl_test.dart b/drift_dev/test/services/schema/verifier_impl_test.dart index b9536fcf..e65862f4 100644 --- a/drift_dev/test/services/schema/verifier_impl_test.dart +++ b/drift_dev/test/services/schema/verifier_impl_test.dart @@ -5,7 +5,11 @@ import 'package:drift_dev/src/services/schema/verifier_impl.dart'; import 'package:test/test.dart'; void main() { - final verifier = SchemaVerifier(_TestHelper()); + final verifier = SchemaVerifier( + _TestHelper(), + setup: (rawDb) => rawDb.createFunction( + functionName: 'test_function', function: (args) => 1), + ); group('startAt', () { test('starts at the requested version', () async { @@ -15,6 +19,12 @@ void main() { expect(details.hadUpgrade, isFalse, reason: 'no upgrade expected'); })); }); + + test('registers custom functions', () async { + final db = (await verifier.startAt(17)).executor; + await db.ensureOpen(_DelegatedUser(17, (_, details) async {})); + await db.runSelect('select test_function()', []); + }); }); group('migrateAndValidate', () { diff --git a/extras/drift_postgres/test/types_test.dart b/extras/drift_postgres/test/types_test.dart index 727480d0..a8cabe21 100644 --- a/extras/drift_postgres/test/types_test.dart +++ b/extras/drift_postgres/test/types_test.dart @@ -26,19 +26,19 @@ void main() { return row.read(expression)!; } + void testWith(CustomSqlType? type, T value) { + test('with variable', () async { + final variable = Variable(value, type); + expect(await eval(variable), value); + }); + + test('with constant', () async { + final constant = Constant(value, type); + expect(await eval(constant), value); + }); + } + group('custom types pass through', () { - void testWith(CustomSqlType type, T value) { - test('with variable', () async { - final variable = Variable(value, type); - expect(await eval(variable), value); - }); - - test('with constant', () async { - final constant = Constant(value, type); - expect(await eval(constant), value); - }); - } - group('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj())); group( 'interval', @@ -60,6 +60,8 @@ void main() { ); }); + group('bytea', () => testWith(null, Uint8List.fromList([1, 2, 3, 4, 5]))); + test('compare datetimes', () async { final time = DateTime.now(); final before = Variable( diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index f05cb2f3..f43d53bd 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,5 +1,6 @@ ## 3.35.0-dev +- Fix parsing binary literals. - Drift extensions: Allow custom class names for `CREATE VIEW` statements. ## 0.34.1 diff --git a/sqlparser/lib/src/reader/tokenizer/scanner.dart b/sqlparser/lib/src/reader/tokenizer/scanner.dart index 4c45b32c..d06096cb 100644 --- a/sqlparser/lib/src/reader/tokenizer/scanner.dart +++ b/sqlparser/lib/src/reader/tokenizer/scanner.dart @@ -290,7 +290,7 @@ class Scanner { } final value = source - .substring(_startOffset + 1, _currentOffset - 1) + .substring(_startOffset + (binary ? 2 : 1), _currentOffset - 1) .replaceAll("''", "'"); tokens.add(StringLiteralToken(value, _currentSpan, binary: binary)); } diff --git a/sqlparser/lib/src/utils/ast_equality.dart b/sqlparser/lib/src/utils/ast_equality.dart index 984d843e..741b7455 100644 --- a/sqlparser/lib/src/utils/ast_equality.dart +++ b/sqlparser/lib/src/utils/ast_equality.dart @@ -642,7 +642,7 @@ class EqualityEnforcingVisitor implements AstVisitor { @override void visitStringLiteral(StringLiteral e, void arg) { final current = _currentAs(e); - _assert(current.value == e.value, e); + _assert(current.value == e.value && current.isBinary == e.isBinary, e); _checkChildren(e); } diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index 59554a36..8996ed66 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -1114,6 +1114,9 @@ class NodeSqlBuilder extends AstVisitor { @override void visitStringLiteral(StringLiteral e, void arg) { + if (e.isBinary) { + symbol('X', spaceBefore: true); + } _stringLiteral(e.value); } diff --git a/sqlparser/test/scanner/single_token_tests.dart b/sqlparser/test/scanner/single_token_test.dart similarity index 87% rename from sqlparser/test/scanner/single_token_tests.dart rename to sqlparser/test/scanner/single_token_test.dart index 4638890b..3a6df59f 100644 --- a/sqlparser/test/scanner/single_token_tests.dart +++ b/sqlparser/test/scanner/single_token_test.dart @@ -86,6 +86,26 @@ void main() { ); }); + test('binary string literal', () { + final scanner = Scanner("X'1234' x'5678'"); + scanner.scanTokens(); + + expect(scanner.tokens, hasLength(3)); + expect( + scanner.tokens[0], + const TypeMatcher() + .having((token) => token.binary, 'binary', isTrue) + .having((token) => token.value, 'value', '1234'), + ); + expect( + scanner.tokens[1], + const TypeMatcher() + .having((token) => token.binary, 'binary', isTrue) + .having((token) => token.value, 'value', '5678'), + ); + expect(scanner.tokens[2].type, TokenType.eof); + }); + group('parses numeric literals', () { void checkLiteral(String lexeme, NumericToken other, num value) { final scanner = Scanner(lexeme)..scanTokens(); diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index 9a2fd353..f4785446 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -568,6 +568,11 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3; testFormat('SELECT a -> b'); testFormat('SELECT a ->> b'); }); + + test('blob literal', () { + testFormat( + "select typeof(X'0100000300000000000000000000803F000000000000003F0000803F');"); + }); }); test('identifiers', () {