Support cast to drift-specific types

This commit is contained in:
Simon Binder 2023-01-06 15:10:03 +01:00
parent 20e6b0d5fe
commit bc325dd31c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
27 changed files with 406 additions and 261 deletions

View File

@ -41,7 +41,7 @@ class _DriftHighlighter extends Highlighter {
void highlight() {
final engine = SqlEngine(
EngineOptions(
useDriftExtensions: true,
driftOptions: const DriftSqlOptions(),
version: SqliteVersion.current,
),
);

View File

@ -2,6 +2,7 @@
## 2.5.0-dev
- Support `MAPPED BY` for individual columns in queries or in views defined with SQL.
- Consistently interpret `CAST (x AS DATETIME)` and `CAST(x AS TEXT)` in drift files.
## 2.4.1

View File

@ -69,7 +69,9 @@ class DriftAnalysisDriver {
SqlEngine newSqlEngine() {
return SqlEngine(
EngineOptions(
useDriftExtensions: true,
driftOptions: DriftSqlOptions(
storeDateTimesAsText: options.storeDateTimeValuesAsText,
),
enabledExtensions: [
DriftOptionsExtension(options),
if (options.hasModule(SqlModule.fts5)) const Fts5Extension(),

View File

@ -69,7 +69,7 @@ class DriftPreprocessor {
DriftBackend backend, Uri uri) async {
final contents = await backend.readAsString(uri);
final engine = SqlEngine(EngineOptions(
useDriftExtensions: true, version: SqliteVersion.current));
driftOptions: const DriftSqlOptions(), version: SqliteVersion.current));
final parsedInput = engine.parseDriftFile(contents);
final directImports = _imports(parsedInput.rootNode, uri).toList();

View File

@ -99,7 +99,7 @@ class MigrateCommand extends MoorCommand {
Future<String> _transformMoorFile(File file) async {
final engine = SqlEngine(EngineOptions(
useDriftExtensions: true, version: SqliteVersion.current));
driftOptions: const DriftSqlOptions(), version: SqliteVersion.current));
final originalContent = await file.readAsString();
var output = originalContent;
final result = engine.parseDriftFile(originalContent);

View File

@ -26,7 +26,7 @@ String placeholderContextName(FoundDartPlaceholder placeholder) {
extension ToSqlText on AstNode {
String toSqlWithoutDriftSpecificSyntax(DriftOptions options) {
final writer = SqlWriter(options);
final writer = SqlWriter(options, escapeForDart: false);
return writer.writeSql(this);
}
}
@ -101,6 +101,36 @@ class SqlWriter extends NodeSqlBuilder {
needsSpace = true;
}
@override
void visitCastExpression(CastExpression e, void arg) {
final schema = SchemaFromCreateTable(
driftExtensions: true,
driftUseTextForDateTime: options.storeDateTimeValuesAsText,
);
final type = schema.resolveColumnType(e.typeName);
final hint = type.hint;
String? overriddenTypeName;
if (hint is IsDateTime) {
overriddenTypeName = options.storeDateTimeValuesAsText ? 'TEXT' : 'INT';
} else if (hint is IsBoolean) {
overriddenTypeName = 'INT';
}
if (overriddenTypeName != null) {
keyword(TokenType.cast);
symbol('(');
visit(e.operand, arg);
keyword(TokenType.as);
symbol(overriddenTypeName, spaceBefore: true);
symbol(')', spaceAfter: true);
} else {
super.visitCastExpression(e, arg);
}
}
@override
void visitColumnConstraint(ColumnConstraint e, void arg) {
if (e is MappedBy) {

View File

@ -1,5 +1,6 @@
@Tags(['analyzer'])
import 'package:drift/drift.dart' as drift;
import 'package:drift_dev/src/analysis/options.dart';
import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:test/test.dart';
@ -220,4 +221,60 @@ TypeConverter<Object, int> createConverter() => throw UnimplementedError();
),
);
});
group('desugars cast', () {
Future<void> expectView(
String definition,
drift.DriftSqlType expectedType,
String expectedSql,
DriftOptions options,
) async {
final backend = TestBackend.inTest(
{'a|lib/a.drift': definition},
options: options,
);
final state = await backend.analyze('package:a/a.drift');
backend.expectNoErrors();
final view = state.analyzedElements.single as DriftView;
final column = view.columns.single;
expect(column.sqlType, expectedType);
expect(
view.source,
isA<SqlViewSource>().having(
(e) => e.sqlCreateViewStmt,
'sqlCreateViewStmt',
expectedSql,
),
);
}
test('to boolean', () async {
await expectView(
'CREATE VIEW x AS SELECT CAST(1 AS BOOLEAN) AS a;',
drift.DriftSqlType.bool,
'CREATE VIEW x AS SELECT CAST(1 AS INT) AS a;',
const DriftOptions.defaults(),
);
});
test('to datetime as int', () async {
await expectView(
'CREATE VIEW x AS SELECT CAST(1 AS DATETIME) AS a;',
drift.DriftSqlType.dateTime,
'CREATE VIEW x AS SELECT CAST(1 AS INT) AS a;',
const DriftOptions.defaults(storeDateTimeValuesAsText: false),
);
});
test('to datetime as text', () async {
await expectView(
"CREATE VIEW x AS SELECT CAST('x' AS DATETIME) AS a;",
drift.DriftSqlType.dateTime,
"CREATE VIEW x AS SELECT CAST('x' AS TEXT) AS a;",
const DriftOptions.defaults(storeDateTimeValuesAsText: true),
);
});
});
}

View File

@ -4,7 +4,8 @@ import 'package:sqlparser/utils/node_to_text.dart';
import 'package:test/test.dart';
void main() {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
final result = engine.parse('CREATE TABLE a (id INTEGER);');
engine.registerTable(const SchemaFromCreateTable()
.read(result.rootNode as CreateTableStatement));

View File

@ -5,6 +5,7 @@ import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:test/test.dart';
import '../../test_utils.dart';
import 'existing_row_classes_test.dart';
import 'utils.dart';
Future<SqlQuery> _handle(String sql) async {
@ -292,4 +293,22 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
final args = query.variables;
expect(args.map((e) => e.sqlType), [DriftSqlType.int, DriftSqlType.string]);
});
test('can cast to DATETIME and BOOLEAN', () async {
final backend = TestBackend.inTest({
'a|lib/a.drift': '''
a: SELECT CAST(1 AS BOOLEAN) AS a, CAST(2 AS DATETIME) as b;
''',
});
final state = await backend.analyze('package:a/a.drift');
final query = state.fileAnalysis!.resolvedQueries.values.single;
final resultSet = query.resultSet!;
expect(resultSet.columns, [
scalarColumn('a').having((e) => e.sqlType, 'sqlType', DriftSqlType.bool),
scalarColumn('b')
.having((e) => e.sqlType, 'sqlType', DriftSqlType.dateTime),
]);
});
}

View File

@ -34,7 +34,8 @@ class ParseDriftFile extends BenchmarkBase {
ParseDriftFile(ScoreEmitter emitter)
: super('Moor file: Parse only', emitter);
final _engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final _engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
@override
void exercise() {

View File

@ -2,10 +2,13 @@ import 'package:meta/meta.dart';
import 'package:sqlparser/sqlparser.dart';
class EngineOptions {
/// If drift extensions are enabled, contains options on how to interpret
/// drift-specific syntax.
///
/// Drift extends the sql grammar a bit to support type converters and other
/// features. Enabling this flag will make this engine parse sql with these
/// extensions enabled.
final bool useDriftExtensions;
final DriftSqlOptions? driftOptions;
/// The target sqlite version.
///
@ -26,8 +29,10 @@ class EngineOptions {
/// function) to the associated handler.
final Map<String, TableValuedFunctionHandler> addedTableFunctions = {};
bool get useDriftExtensions => driftOptions != null;
EngineOptions({
this.useDriftExtensions = false,
this.driftOptions,
List<Extension> enabledExtensions = const [],
this.version = SqliteVersion.minimum,
}) : enabledExtensions = _allExtensions(enabledExtensions, version) {
@ -66,6 +71,15 @@ class EngineOptions {
}
}
/// Drift-specific parsing and analysis options that are not enabled by default.
class DriftSqlOptions {
final bool storeDateTimesAsText;
const DriftSqlOptions({
this.storeDateTimesAsText = false,
});
}
/// The assumed version of `sqlite3`.
///
/// This library can provide analysis hints when using sqlite features newer

View File

@ -34,8 +34,12 @@ class SqlEngine {
/// The returned reader can be used to read the table structure from a
/// [TableInducingStatement] by using [SchemaFromCreateTable.read].
SchemaFromCreateTable get schemaReader {
return _schemaReader ??=
SchemaFromCreateTable(driftExtensions: options.useDriftExtensions);
final driftOptions = options.driftOptions;
return _schemaReader ??= SchemaFromCreateTable(
driftExtensions: driftOptions != null,
driftUseTextForDateTime: driftOptions?.storeDateTimesAsText == true,
);
}
/// Registers the [table], which means that it can later be used in sql

File diff suppressed because it is too large Load Diff

View File

@ -26,14 +26,15 @@ void main() {
});
test('does not register the same result set multiple times', () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
..registerTableFromSql('''
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTableFromSql('''
CREATE TABLE with_defaults (
a TEXT DEFAULT 'something',
b INT UNIQUE
);
''')
..registerTableFromSql('''
..registerTableFromSql('''
CREATE TABLE with_constraints (
a TEXT,
b INT NOT NULL,

View File

@ -6,8 +6,8 @@ import 'utils.dart';
void main() {
final oldEngine = SqlEngine(EngineOptions(version: SqliteVersion.v3_35));
final engine = SqlEngine(EngineOptions(version: SqliteVersion.v3_37));
final engineInDriftMode = SqlEngine(
EngineOptions(version: SqliteVersion.v3_37, useDriftExtensions: true));
final engineInDriftMode = SqlEngine(EngineOptions(
version: SqliteVersion.v3_37, driftOptions: const DriftSqlOptions()));
group('using STRICT', () {
test('with an old sqlite3 version', () {

View File

@ -63,8 +63,9 @@ void main() {
});
test("resolved columns don't include drift nested results", () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
..registerTable(demoTable);
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTable(demoTable);
final context = engine.analyze('SELECT demo.** FROM demo;');
@ -105,9 +106,10 @@ void main() {
});
test('resolves columns from nested results', () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
..registerTable(demoTable)
..registerTable(anotherTable);
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTable(demoTable)
..registerTable(anotherTable);
final context = engine.analyze('SELECT SUM(*) AS rst FROM '
'(SELECT COUNT(*) FROM demo UNION ALL SELECT COUNT(*) FROM tbl);');
@ -123,8 +125,9 @@ void main() {
});
test('resolves columns in nested queries', () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
..registerTable(demoTable);
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTable(demoTable);
final context =
engine.analyze('SELECT content, LIST(SELECT id FROM demo) FROM demo');

View File

@ -35,15 +35,16 @@ void main() {
test('regression test for #1188', () {
// Test for https://github.com/simolus3/drift/issues/1188
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
..registerTableFromSql('''
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTableFromSql('''
CREATE TABLE IF NOT EXISTS "employees" (
"id" INTEGER NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL UNIQUE,
"manager_id" INTEGER
);
''')
..registerTableFromSql('''
..registerTableFromSql('''
CREATE TABLE IF NOT EXISTS "employee_notes" (
"employee_id" INTEGER NOT NULL PRIMARY KEY,
"note" TEXT
@ -85,14 +86,15 @@ void main() {
test('regression test for #1234', () {
// https://github.com/simolus3/drift/issues/1234#issuecomment-853270925
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
..registerTableFromSql('''
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTableFromSql('''
CREATE TABLE inboxes (
id TEXT PRIMARY KEY NOT NULL,
group_id TEXT NOT NULL
);
''')
..registerTableFromSql('''
..registerTableFromSql('''
CREATE TABLE assignable_users (
user_id TEXT NOT NULL,
inbox_id TEXT NOT NULL
@ -116,8 +118,8 @@ void main() {
test('regression test for #1096', () {
// https://github.com/simolus3/drift/issues/1096#issuecomment-931378474
final engine = SqlEngine(
EngineOptions(useDriftExtensions: true, version: SqliteVersion.v3_35))
final engine = SqlEngine(EngineOptions(
driftOptions: const DriftSqlOptions(), version: SqliteVersion.v3_35))
..registerTableFromSql('''
CREATE TABLE downloads (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -159,8 +161,8 @@ CREATE TABLE downloads (
test('regression test for #1858', () {
// https://github.com/simolus3/drift/issues/1858
final engine = SqlEngine(
EngineOptions(useDriftExtensions: true, version: SqliteVersion.v3_38));
final engine = SqlEngine(EngineOptions(
driftOptions: const DriftSqlOptions(), version: SqliteVersion.v3_38));
engine.registerTableFromSql('''
CREATE TABLE IF NOT EXISTS contract_has_add_fees

View File

@ -99,7 +99,8 @@ void main() {
});
test('supports booleans when drift extensions are enabled', () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
final stmt = engine.parse('''
CREATE TABLE foo (
a BOOL, b DATETIME, c DATE, d BOOLEAN NOT NULL

View File

@ -5,7 +5,7 @@ import 'package:test/test.dart';
import '../data.dart';
void main() {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true))
final engine = SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
..registerTable(demoTable)
..registerTable(anotherTable);

View File

@ -7,7 +7,8 @@ import 'package:test/test.dart';
ComputedSuggestions completionsFor(String driftFile,
{void Function(SqlEngine)? setup}) {
final position = driftFile.indexOf('^');
final engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
setup?.call(engine);
final result = engine.parseDriftFile(driftFile.replaceFirst('^', ''));

View File

@ -65,8 +65,9 @@ void main() {
});
test('never allows drift extensions', () {
final result = SqlEngine(EngineOptions(useDriftExtensions: true))
.parseColumnConstraints('MAPPED BY `myconverter()`');
final result =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parseColumnConstraints('MAPPED BY `myconverter()`');
expect(result.errors, [
isA<ParsingError>().having(
(e) => e.message, 'message', contains('Expected a constraint')),

View File

@ -162,8 +162,9 @@ END;
test("reports error when the statement can't be parsed", () {
// regression test for https://github.com/simolus3/drift/issues/280#issuecomment-570789454
final parsed = SqlEngine(EngineOptions(useDriftExtensions: true))
.parseDriftFile('name: NSERT INTO foo DEFAULT VALUES;');
final parsed =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parseDriftFile('name: NSERT INTO foo DEFAULT VALUES;');
expect(
parsed.errors,
@ -182,7 +183,8 @@ END;
});
test('syntax errors contain correct position', () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
final result = engine.parseDriftFile('''
worksByComposer:
SELECT DISTINCT A.* FROM works A, works B ON A.id =
@ -222,7 +224,8 @@ SELECT DISTINCT A.* FROM works A, works B ON A.id =
test('allows statements to appear in any order', () {
final result =
SqlEngine(EngineOptions(useDriftExtensions: true)).parseDriftFile('''
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parseDriftFile('''
CREATE TABLE foo (
a INTEGER NOT NULL
);

View File

@ -41,7 +41,8 @@ CREATE TABLE foo (
});
test('parses trailing comma with error', () {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
final result = engine.parseDriftFile('''
CREATE TABLE foo (

View File

@ -6,7 +6,7 @@ import '../utils.dart';
void main() {
test('parses nested query statements', () {
final stmt = SqlEngine(EngineOptions(useDriftExtensions: true))
final stmt = SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parse('SELECT LIST(SELECT * FROM test) FROM test')
.rootNode as SelectStatement;
@ -23,7 +23,7 @@ void main() {
});
test('parses nested query statements with as', () {
final stmt = SqlEngine(EngineOptions(useDriftExtensions: true))
final stmt = SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parse('SELECT LIST(SELECT * FROM test) AS newname FROM test')
.rootNode as SelectStatement;

View File

@ -31,9 +31,10 @@ void main() {
});
test('parses clauses with NULLS FIRST or NULLS LAST', () {
final parsed = SqlEngine(EngineOptions(useDriftExtensions: true))
.parse(r'SELECT * FROM tbl ORDER BY $a NULLS LAST, b NULLS FIRST')
.rootNode as SelectStatement;
final parsed =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parse(r'SELECT * FROM tbl ORDER BY $a NULLS LAST, b NULLS FIRST')
.rootNode as SelectStatement;
enforceHasSpan(parsed);
enforceEqual(

View File

@ -25,7 +25,7 @@ IdentifierToken identifier(String content) {
}
DriftFile parseDrift(String content) {
return SqlEngine(EngineOptions(useDriftExtensions: true))
return SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()))
.parseDriftFile(content)
.rootNode as DriftFile;
}
@ -37,8 +37,9 @@ void testDriftFile(String driftFile, DriftFile expected) {
}
void testStatement(String sql, AstNode expected, {bool driftMode = false}) {
final result =
SqlEngine(EngineOptions(useDriftExtensions: driftMode)).parse(sql);
final result = SqlEngine(EngineOptions(
driftOptions: driftMode ? const DriftSqlOptions() : null))
.parse(sql);
expect(result.errors, isEmpty);
final parsed = result.rootNode;

View File

@ -6,7 +6,8 @@ import 'package:test/test.dart';
enum _ParseKind { statement, driftFile }
void main() {
final engine = SqlEngine(EngineOptions(useDriftExtensions: true));
final engine =
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
void testFormat(String input,
{_ParseKind kind = _ParseKind.statement, String? expectedOutput}) {