mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into modular-generation
This commit is contained in:
commit
0adb398ddc
|
@ -32,7 +32,7 @@ typedef DatabaseSetup = void Function(Database database);
|
||||||
/// Dart VM or an AOT compiled Dart/Flutter application.
|
/// Dart VM or an AOT compiled Dart/Flutter application.
|
||||||
class NativeDatabase extends DelegatedDatabase {
|
class NativeDatabase extends DelegatedDatabase {
|
||||||
NativeDatabase._(DatabaseDelegate delegate, bool logStatements)
|
NativeDatabase._(DatabaseDelegate delegate, bool logStatements)
|
||||||
: super(delegate, isSequential: true, logStatements: logStatements);
|
: super(delegate, isSequential: false, logStatements: logStatements);
|
||||||
|
|
||||||
/// Creates a database that will store its result in the [file], creating it
|
/// Creates a database that will store its result in the [file], creating it
|
||||||
/// if it doesn't exist.
|
/// if it doesn't exist.
|
||||||
|
@ -162,7 +162,36 @@ class _NativeDelegate extends Sqlite3Delegate<Database> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void beforeClose(Database database) {
|
Future<void> runBatched(BatchedStatements statements) {
|
||||||
tracker.markClosed(database);
|
return Future.sync(() => runBatchSync(statements));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runCustom(String statement, List<Object?> args) {
|
||||||
|
return Future.sync(() => runWithArgsSync(statement, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runInsert(String statement, List<Object?> args) {
|
||||||
|
return Future.sync(() {
|
||||||
|
runWithArgsSync(statement, args);
|
||||||
|
return database.lastInsertRowId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runUpdate(String statement, List<Object?> args) {
|
||||||
|
return Future.sync(() {
|
||||||
|
runWithArgsSync(statement, args);
|
||||||
|
return database.getUpdatedRows();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
if (closeUnderlyingWhenClosed) {
|
||||||
|
tracker.markClosed(database);
|
||||||
|
database.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,21 @@ import 'native_functions.dart';
|
||||||
/// through `package:js`.
|
/// through `package:js`.
|
||||||
abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
extends DatabaseDelegate {
|
extends DatabaseDelegate {
|
||||||
late DB _db;
|
late DB database;
|
||||||
|
|
||||||
bool _hasCreatedDatabase = false;
|
bool _hasCreatedDatabase = false;
|
||||||
bool _isOpen = false;
|
bool _isOpen = false;
|
||||||
|
|
||||||
final void Function(DB)? _setup;
|
final void Function(DB)? _setup;
|
||||||
final bool _closeUnderlyingWhenClosed;
|
final bool closeUnderlyingWhenClosed;
|
||||||
|
|
||||||
/// A delegate that will call [openDatabase] to open the database.
|
/// A delegate that will call [openDatabase] to open the database.
|
||||||
Sqlite3Delegate(this._setup) : _closeUnderlyingWhenClosed = true;
|
Sqlite3Delegate(this._setup) : closeUnderlyingWhenClosed = true;
|
||||||
|
|
||||||
/// A delegate using an underlying sqlite3 database object that has already
|
/// A delegate using an underlying sqlite3 database object that has already
|
||||||
/// been opened.
|
/// been opened.
|
||||||
Sqlite3Delegate.opened(this._db, this._setup, this._closeUnderlyingWhenClosed)
|
Sqlite3Delegate.opened(
|
||||||
|
this.database, this._setup, this.closeUnderlyingWhenClosed)
|
||||||
: _hasCreatedDatabase = true {
|
: _hasCreatedDatabase = true {
|
||||||
_initializeDatabase();
|
_initializeDatabase();
|
||||||
}
|
}
|
||||||
|
@ -36,10 +37,6 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
/// the right sqlite3 database instance.
|
/// the right sqlite3 database instance.
|
||||||
DB openDatabase();
|
DB openDatabase();
|
||||||
|
|
||||||
/// This method may optionally be overridden by the platform-specific
|
|
||||||
/// implementation to get notified before a database would be closed.
|
|
||||||
void beforeClose(DB database) {}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TransactionDelegate get transactionDelegate => const NoTransactionDelegate();
|
TransactionDelegate get transactionDelegate => const NoTransactionDelegate();
|
||||||
|
|
||||||
|
@ -49,12 +46,6 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
@override
|
@override
|
||||||
Future<bool> get isOpen => Future.value(_isOpen);
|
Future<bool> get isOpen => Future.value(_isOpen);
|
||||||
|
|
||||||
/// Flush pending writes to the file system on platforms where that is
|
|
||||||
/// necessary.
|
|
||||||
///
|
|
||||||
/// At the moment, we only support this for the WASM backend.
|
|
||||||
FutureOr<void> flush() => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> open(QueryExecutorUser db) async {
|
Future<void> open(QueryExecutorUser db) async {
|
||||||
if (!_hasCreatedDatabase) {
|
if (!_hasCreatedDatabase) {
|
||||||
|
@ -70,22 +61,21 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
assert(!_hasCreatedDatabase);
|
assert(!_hasCreatedDatabase);
|
||||||
_hasCreatedDatabase = true;
|
_hasCreatedDatabase = true;
|
||||||
|
|
||||||
_db = openDatabase();
|
database = openDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeDatabase() {
|
void _initializeDatabase() {
|
||||||
_db.useNativeFunctions();
|
database.useNativeFunctions();
|
||||||
_setup?.call(_db);
|
_setup?.call(database);
|
||||||
versionDelegate = _VmVersionDelegate(_db);
|
versionDelegate = _VmVersionDelegate(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void runBatchSync(BatchedStatements statements) {
|
||||||
Future<void> runBatched(BatchedStatements statements) async {
|
|
||||||
final prepared = <CommonPreparedStatement>[];
|
final prepared = <CommonPreparedStatement>[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (final stmt in statements.statements) {
|
for (final stmt in statements.statements) {
|
||||||
prepared.add(_db.prepare(stmt, checkNoTail: true));
|
prepared.add(database.prepare(stmt, checkNoTail: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final application in statements.arguments) {
|
for (final application in statements.arguments) {
|
||||||
|
@ -98,49 +88,24 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
stmt.dispose();
|
stmt.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isInTransaction) {
|
|
||||||
await flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _runWithArgs(String statement, List<Object?> args) async {
|
void runWithArgsSync(String statement, List<Object?> args) {
|
||||||
if (args.isEmpty) {
|
if (args.isEmpty) {
|
||||||
_db.execute(statement);
|
database.execute(statement);
|
||||||
} else {
|
} else {
|
||||||
final stmt = _db.prepare(statement, checkNoTail: true);
|
final stmt = database.prepare(statement, checkNoTail: true);
|
||||||
try {
|
try {
|
||||||
stmt.execute(args);
|
stmt.execute(args);
|
||||||
} finally {
|
} finally {
|
||||||
stmt.dispose();
|
stmt.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isInTransaction) {
|
|
||||||
await flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> runCustom(String statement, List<Object?> args) async {
|
|
||||||
await _runWithArgs(statement, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> runInsert(String statement, List<Object?> args) async {
|
|
||||||
await _runWithArgs(statement, args);
|
|
||||||
return _db.lastInsertRowId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> runUpdate(String statement, List<Object?> args) async {
|
|
||||||
await _runWithArgs(statement, args);
|
|
||||||
return _db.getUpdatedRows();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
||||||
final stmt = _db.prepare(statement, checkNoTail: true);
|
final stmt = database.prepare(statement, checkNoTail: true);
|
||||||
try {
|
try {
|
||||||
final result = stmt.select(args);
|
final result = stmt.select(args);
|
||||||
return QueryResult.fromRows(result.toList());
|
return QueryResult.fromRows(result.toList());
|
||||||
|
@ -148,16 +113,6 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
stmt.dispose();
|
stmt.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_closeUnderlyingWhenClosed) {
|
|
||||||
beforeClose(_db);
|
|
||||||
_db.dispose();
|
|
||||||
|
|
||||||
await flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VmVersionDelegate extends DynamicVersionDelegate {
|
class _VmVersionDelegate extends DynamicVersionDelegate {
|
||||||
|
|
|
@ -85,8 +85,49 @@ class _WasmDelegate extends Sqlite3Delegate<CommonDatabase> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _flush() async {
|
||||||
Future<void> flush() async {
|
|
||||||
await _fileSystem?.flush();
|
await _fileSystem?.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _runWithArgs(String statement, List<Object?> args) async {
|
||||||
|
runWithArgsSync(statement, args);
|
||||||
|
|
||||||
|
if (!isInTransaction) {
|
||||||
|
await _flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runCustom(String statement, List<Object?> args) async {
|
||||||
|
await _runWithArgs(statement, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runInsert(String statement, List<Object?> args) async {
|
||||||
|
await _runWithArgs(statement, args);
|
||||||
|
return database.lastInsertRowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runUpdate(String statement, List<Object?> args) async {
|
||||||
|
await _runWithArgs(statement, args);
|
||||||
|
return database.getUpdatedRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBatched(BatchedStatements statements) async {
|
||||||
|
runBatchSync(statements);
|
||||||
|
|
||||||
|
if (!isInTransaction) {
|
||||||
|
await _flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
if (closeUnderlyingWhenClosed) {
|
||||||
|
database.dispose();
|
||||||
|
await _flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group('delegates queries', () {
|
group('delegates queries', () {
|
||||||
void _runTests(bool sequential) {
|
void runTests(bool sequential) {
|
||||||
test('when sequential = $sequential', () async {
|
test('when sequential = $sequential', () async {
|
||||||
final db = DelegatedDatabase(delegate, isSequential: sequential);
|
final db = DelegatedDatabase(delegate, isSequential: sequential);
|
||||||
await db.ensureOpen(_FakeExecutorUser());
|
await db.ensureOpen(_FakeExecutorUser());
|
||||||
|
@ -61,8 +61,8 @@ void main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_runTests(false);
|
runTests(false);
|
||||||
_runTests(true);
|
runTests(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('migrations', () {
|
group('migrations', () {
|
||||||
|
|
|
@ -20,23 +20,23 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
group('pow', () {
|
group('pow', () {
|
||||||
dynamic _resultOfPow(String a, String b) {
|
dynamic resultOfPow(String a, String b) {
|
||||||
return selectSingle('pow($a, $b)');
|
return selectSingle('pow($a, $b)');
|
||||||
}
|
}
|
||||||
|
|
||||||
test('returns null when any argument is null', () {
|
test('returns null when any argument is null', () {
|
||||||
expect(_resultOfPow('null', 'null'), isNull);
|
expect(resultOfPow('null', 'null'), isNull);
|
||||||
expect(_resultOfPow('3', 'null'), isNull);
|
expect(resultOfPow('3', 'null'), isNull);
|
||||||
expect(_resultOfPow('null', '3'), isNull);
|
expect(resultOfPow('null', '3'), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns correct results', () {
|
test('returns correct results', () {
|
||||||
expect(_resultOfPow('10', '0'), 1);
|
expect(resultOfPow('10', '0'), 1);
|
||||||
expect(_resultOfPow('0', '10'), 0);
|
expect(resultOfPow('0', '10'), 0);
|
||||||
expect(_resultOfPow('0', '0'), 1);
|
expect(resultOfPow('0', '0'), 1);
|
||||||
expect(_resultOfPow('2', '5'), 32);
|
expect(resultOfPow('2', '5'), 32);
|
||||||
expect(_resultOfPow('3.5', '2'), 12.25);
|
expect(resultOfPow('3.5', '2'), 12.25);
|
||||||
expect(_resultOfPow('10', '-1'), 0.1);
|
expect(resultOfPow('10', '-1'), 0.1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -33,25 +33,25 @@ void main() {
|
||||||
final parser = DriftDartParser(ParseDartStep(
|
final parser = DriftDartParser(ParseDartStep(
|
||||||
task, session.registerFile(input), library, await task.helper));
|
task, session.registerFile(input), library, await task.helper));
|
||||||
|
|
||||||
Future<MethodDeclaration> _loadDeclaration(Element element) async {
|
Future<MethodDeclaration> loadDeclaration(Element element) async {
|
||||||
final node = await parser.loadElementDeclaration(element);
|
final node = await parser.loadElementDeclaration(element);
|
||||||
return node as MethodDeclaration;
|
return node as MethodDeclaration;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _verifyReturnExpressionMatches(
|
Future<void> verifyReturnExpressionMatches(
|
||||||
Element element, String source) async {
|
Element element, String source) async {
|
||||||
final node = await _loadDeclaration(element);
|
final node = await loadDeclaration(element);
|
||||||
expect(parser.returnExpressionOfMethod(node)!.toSource(), source);
|
expect(parser.returnExpressionOfMethod(node)!.toSource(), source);
|
||||||
}
|
}
|
||||||
|
|
||||||
final testClass = library.getClass('Test');
|
final testClass = library.getClass('Test');
|
||||||
|
|
||||||
await _verifyReturnExpressionMatches(
|
await verifyReturnExpressionMatches(
|
||||||
testClass!.getGetter('getter')!, "'foo'");
|
testClass!.getGetter('getter')!, "'foo'");
|
||||||
await _verifyReturnExpressionMatches(
|
await verifyReturnExpressionMatches(
|
||||||
testClass.getMethod('function')!, "'bar'");
|
testClass.getMethod('function')!, "'bar'");
|
||||||
|
|
||||||
final invalidDecl = await _loadDeclaration(testClass.getMethod('invalid')!);
|
final invalidDecl = await loadDeclaration(testClass.getMethod('invalid')!);
|
||||||
expect(parser.returnExpressionOfMethod(invalidDecl), isNull);
|
expect(parser.returnExpressionOfMethod(invalidDecl), isNull);
|
||||||
expect(parser.step.errors.errors, isNotEmpty);
|
expect(parser.step.errors.errors, isNotEmpty);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:test/test.dart';
|
||||||
import '../utils.dart';
|
import '../utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Future<BaseDriftAccessor> _analyzeQueries(String moorFile) async {
|
Future<BaseDriftAccessor> analyzeQueries(String moorFile) async {
|
||||||
final state = TestState.withContent({'foo|lib/a.moor': moorFile});
|
final state = TestState.withContent({'foo|lib/a.moor': moorFile});
|
||||||
|
|
||||||
final result = await state.analyze('package:foo/a.moor');
|
final result = await state.analyze('package:foo/a.moor');
|
||||||
|
@ -19,7 +19,7 @@ void main() {
|
||||||
|
|
||||||
group('does not allow custom classes for queries', () {
|
group('does not allow custom classes for queries', () {
|
||||||
test('with a single column', () async {
|
test('with a single column', () async {
|
||||||
final queries = await _analyzeQueries('''
|
final queries = await analyzeQueries('''
|
||||||
myQuery AS MyResult: SELECT 1;
|
myQuery AS MyResult: SELECT 1;
|
||||||
''');
|
''');
|
||||||
|
|
||||||
|
@ -35,9 +35,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('matching a table', () async {
|
test('matching a table', () async {
|
||||||
final queries = await _analyzeQueries('''
|
final queries = await analyzeQueries('''
|
||||||
CREATE TABLE demo (id INTEGER NOT NULL PRIMARY KEY);
|
CREATE TABLE demo (id INTEGER NOT NULL PRIMARY KEY);
|
||||||
|
|
||||||
myQuery AS MyResult: SELECT id FROM demo;
|
myQuery AS MyResult: SELECT id FROM demo;
|
||||||
''');
|
''');
|
||||||
|
|
||||||
|
@ -57,25 +57,25 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reports error for queries with different result sets', () async {
|
test('reports error for queries with different result sets', () async {
|
||||||
final queries = await _analyzeQueries('''
|
final queries = await analyzeQueries('''
|
||||||
CREATE TABLE points (
|
CREATE TABLE points (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
lat REAL,
|
lat REAL,
|
||||||
long REAL
|
long REAL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE routes (
|
CREATE TABLE routes (
|
||||||
"start" INTEGER REFERENCES points (id),
|
"start" INTEGER REFERENCES points (id),
|
||||||
"end" INTEGER REFERENCES points (id),
|
"end" INTEGER REFERENCES points (id),
|
||||||
PRIMARY KEY ("start", "end")
|
PRIMARY KEY ("start", "end")
|
||||||
);
|
);
|
||||||
|
|
||||||
difCols1 AS DifferentColumns: SELECT id, lat FROM points;
|
difCols1 AS DifferentColumns: SELECT id, lat FROM points;
|
||||||
difCols2 AS DifferentColumns: SELECT id, long FROM points;
|
difCols2 AS DifferentColumns: SELECT id, long FROM points;
|
||||||
|
|
||||||
difNested1 AS DifferentNested: SELECT
|
difNested1 AS DifferentNested: SELECT
|
||||||
start.** FROM routes INNER JOIN points start ON start.id = routes.start;
|
start.** FROM routes INNER JOIN points start ON start.id = routes.start;
|
||||||
difNested2 AS DifferentNested: SELECT
|
difNested2 AS DifferentNested: SELECT
|
||||||
"end".** FROM routes INNER JOIN points "end" ON "end".id = routes."end";
|
"end".** FROM routes INNER JOIN points "end" ON "end".id = routes."end";
|
||||||
''');
|
''');
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ void main() {
|
||||||
engine.registerTable(const SchemaFromCreateTable()
|
engine.registerTable(const SchemaFromCreateTable()
|
||||||
.read(result.rootNode as CreateTableStatement));
|
.read(result.rootNode as CreateTableStatement));
|
||||||
|
|
||||||
void _test(String input, String output) {
|
void checkTransformation(String input, String output) {
|
||||||
final node = engine.analyze(input).root;
|
final node = engine.analyze(input).root;
|
||||||
final transformer = ExplicitAliasTransformer();
|
final transformer = ExplicitAliasTransformer();
|
||||||
transformer.rewrite(node);
|
transformer.rewrite(node);
|
||||||
|
@ -18,29 +18,29 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
test('rewrites simple queries', () {
|
test('rewrites simple queries', () {
|
||||||
_test('SELECT 1 + 2', 'SELECT 1 + 2 AS _c0');
|
checkTransformation('SELECT 1 + 2', 'SELECT 1 + 2 AS _c0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not rewrite simple references', () {
|
test('does not rewrite simple references', () {
|
||||||
_test('SELECT id FROM a', 'SELECT id FROM a');
|
checkTransformation('SELECT id FROM a', 'SELECT id FROM a');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rewrites references', () {
|
test('rewrites references', () {
|
||||||
_test('SELECT "1+2" FROM (SELECT 1+2)',
|
checkTransformation('SELECT "1+2" FROM (SELECT 1+2)',
|
||||||
'SELECT _c0 FROM (SELECT 1 + 2 AS _c0)');
|
'SELECT _c0 FROM (SELECT 1 + 2 AS _c0)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not rewrite subquery expressions', () {
|
test('does not rewrite subquery expressions', () {
|
||||||
_test('SELECT (SELECT 1)', 'SELECT (SELECT 1) AS _c0');
|
checkTransformation('SELECT (SELECT 1)', 'SELECT (SELECT 1) AS _c0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rewrites compound select statements', () {
|
test('rewrites compound select statements', () {
|
||||||
_test("SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar'",
|
checkTransformation("SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar'",
|
||||||
"SELECT 1 + 2 AS _c0, 'foo' AS _c1 UNION ALL SELECT 3 + 4, 'bar'");
|
"SELECT 1 + 2 AS _c0, 'foo' AS _c1 UNION ALL SELECT 3 + 4, 'bar'");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rewrites references for compount select statements', () {
|
test('rewrites references for compount select statements', () {
|
||||||
_test(
|
checkTransformation(
|
||||||
'''
|
'''
|
||||||
SELECT "1 + 2", "'foo'" FROM
|
SELECT "1 + 2", "'foo'" FROM
|
||||||
(SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar')
|
(SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar')
|
||||||
|
@ -51,7 +51,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rewrites references for compount select statements', () {
|
test('rewrites references for compount select statements', () {
|
||||||
_test(
|
checkTransformation(
|
||||||
'''
|
'''
|
||||||
WITH
|
WITH
|
||||||
foo AS (SELECT 2 * 3 UNION ALL SELECT 3)
|
foo AS (SELECT 2 * 3 UNION ALL SELECT 3)
|
||||||
|
@ -63,7 +63,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not rewrite compound select statements with explicit names', () {
|
test('does not rewrite compound select statements with explicit names', () {
|
||||||
_test(
|
checkTransformation(
|
||||||
'''
|
'''
|
||||||
WITH
|
WITH
|
||||||
foo(x) AS (SELECT 2 * 3 UNION ALL SELECT 3)
|
foo(x) AS (SELECT 2 * 3 UNION ALL SELECT 3)
|
||||||
|
@ -75,7 +75,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rewrites RETURNING clauses', () {
|
test('rewrites RETURNING clauses', () {
|
||||||
_test(
|
checkTransformation(
|
||||||
'INSERT INTO a VALUES (1), (2) RETURNING id * 3',
|
'INSERT INTO a VALUES (1), (2) RETURNING id * 3',
|
||||||
'INSERT INTO a VALUES (1), (2) RETURNING id * 3 AS _c0',
|
'INSERT INTO a VALUES (1), (2) RETURNING id * 3 AS _c0',
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,7 +101,7 @@ void main() {
|
||||||
|
|
||||||
tearDown(() => state.close());
|
tearDown(() => state.close());
|
||||||
|
|
||||||
Future<void> _runTest(DriftOptions options, Matcher expectation) async {
|
Future<void> runTest(DriftOptions options, Matcher expectation) async {
|
||||||
final file = await state.analyze('package:a/main.moor');
|
final file = await state.analyze('package:a/main.moor');
|
||||||
final fileState = file.currentResult as ParsedDriftFile;
|
final fileState = file.currentResult as ParsedDriftFile;
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
test('with the new query generator', () {
|
test('with the new query generator', () {
|
||||||
return _runTest(
|
return runTest(
|
||||||
const DriftOptions.defaults(),
|
const DriftOptions.defaults(),
|
||||||
allOf(
|
allOf(
|
||||||
contains(r'var $arrayStartIndex = 3;'),
|
contains(r'var $arrayStartIndex = 3;'),
|
||||||
|
@ -139,10 +139,10 @@ void main() {
|
||||||
c TEXT
|
c TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
query:
|
query:
|
||||||
SELECT
|
SELECT
|
||||||
parent.a,
|
parent.a,
|
||||||
LIST(SELECT b, c FROM tbl WHERE a = :a OR a = parent.a AND b = :b)
|
LIST(SELECT b, c FROM tbl WHERE a = :a OR a = parent.a AND b = :b)
|
||||||
FROM tbl AS parent WHERE parent.a = :a;
|
FROM tbl AS parent WHERE parent.a = :a;
|
||||||
''',
|
''',
|
||||||
});
|
});
|
||||||
|
@ -150,7 +150,7 @@ void main() {
|
||||||
|
|
||||||
tearDown(() => state.close());
|
tearDown(() => state.close());
|
||||||
|
|
||||||
Future<void> _runTest(
|
Future<void> runTest(
|
||||||
DriftOptions options, List<Matcher> expectation) async {
|
DriftOptions options, List<Matcher> expectation) async {
|
||||||
final file = await state.analyze('package:a/main.moor');
|
final file = await state.analyze('package:a/main.moor');
|
||||||
final fileState = file.currentResult as ParsedDriftFile;
|
final fileState = file.currentResult as ParsedDriftFile;
|
||||||
|
@ -167,7 +167,7 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
test('should generate correct queries with variables', () {
|
test('should generate correct queries with variables', () {
|
||||||
return _runTest(
|
return runTest(
|
||||||
const DriftOptions.defaults(),
|
const DriftOptions.defaults(),
|
||||||
[
|
[
|
||||||
contains(
|
contains(
|
||||||
|
@ -187,7 +187,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should generate correct data class', () {
|
test('should generate correct data class', () {
|
||||||
return _runTest(
|
return runTest(
|
||||||
const DriftOptions.defaults(),
|
const DriftOptions.defaults(),
|
||||||
[
|
[
|
||||||
contains('QueryNestedQuery0({this.b,this.c,})'),
|
contains('QueryNestedQuery0({this.b,this.c,})'),
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"categories","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment","primary-key"]},{"name":"name","getter_name":"name","moor_type":"ColumnType.text","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const ColorConverter()","dart_type_name":"Color"}}],"is_virtual":false}},{"id":1,"references":[0],"type":"table","data":{"name":"todo_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment","primary-key"]},{"name":"description","getter_name":"description","moor_type":"ColumnType.text","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"category","getter_name":"category","moor_type":"ColumnType.integer","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES categories (id)","default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"due_date","getter_name":"dueDate","moor_type":"ColumnType.datetime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false}},{"id":2,"references":[1],"type":"table","data":{"name":"text_entries","was_declared_in_moor":true,"columns":[{"name":"description","getter_name":"description","moor_type":"ColumnType.text","nullable":false,"customConstraints":"","default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":true,"create_virtual_stmt":"CREATE VIRTUAL TABLE text_entries USING fts5 (\n description,\n content=todo_entries,\n content_rowid=id\n);"}},{"id":3,"references":[1,2],"type":"trigger","data":{"on":1,"refences_in_body":[2,1],"name":"todos_insert","sql":"CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries (\"rowid\", description) VALUES (new.id, new.description);END"}},{"id":4,"references":[1,2],"type":"trigger","data":{"on":1,"refences_in_body":[2,1],"name":"todos_delete","sql":"CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, \"rowid\", description) VALUES ('delete', old.id, old.description);END"}},{"id":5,"references":[1,2],"type":"trigger","data":{"on":1,"refences_in_body":[2,1],"name":"todos_update","sql":"CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, \"rowid\", description) VALUES ('delete', new.id, new.description);INSERT INTO text_entries (\"rowid\", description) VALUES (new.id, new.description);END"}}]}
|
|
@ -16,15 +16,29 @@ class AppDatabase extends _$AppDatabase {
|
||||||
: super.connect(connection);
|
: super.connect(connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
return MigrationStrategy(
|
return MigrationStrategy(
|
||||||
onUpgrade: ((m, from, to) async {
|
onUpgrade: ((m, from, to) async {
|
||||||
if (from == 1) {
|
for (var step = from + 1; step <= to; step++) {
|
||||||
// The todoEntries.dueDate column was added in version 2.
|
switch (step) {
|
||||||
await m.addColumn(todoEntries, todoEntries.dueDate);
|
case 2:
|
||||||
|
// The todoEntries.dueDate column was added in version 2.
|
||||||
|
await m.addColumn(todoEntries, todoEntries.dueDate);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// New triggers were added in version 3:
|
||||||
|
await m.create(todosDelete);
|
||||||
|
await m.create(todosUpdate);
|
||||||
|
|
||||||
|
// Also, the `REFERENCES` constraint was added to
|
||||||
|
// [TodoEntries.category]. Run a table migration to rebuild all
|
||||||
|
// column constraints without loosing data.
|
||||||
|
await m.alterTable(TableMigration(todoEntries));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
|
|
|
@ -623,6 +623,12 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
late final Trigger todosInsert = Trigger(
|
late final Trigger todosInsert = Trigger(
|
||||||
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END',
|
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END',
|
||||||
'todos_insert');
|
'todos_insert');
|
||||||
|
late final Trigger todosDelete = Trigger(
|
||||||
|
'CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', old.id, old.description);END',
|
||||||
|
'todos_delete');
|
||||||
|
late final Trigger todosUpdate = Trigger(
|
||||||
|
'CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', new.id, new.description);INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END',
|
||||||
|
'todos_update');
|
||||||
Selectable<CategoriesWithCountResult> _categoriesWithCount() {
|
Selectable<CategoriesWithCountResult> _categoriesWithCount() {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT c.*, (SELECT COUNT(*) FROM todo_entries WHERE category = c.id) AS amount FROM categories AS c UNION ALL SELECT NULL, NULL, NULL, (SELECT COUNT(*) FROM todo_entries WHERE category IS NULL)',
|
'SELECT c.*, (SELECT COUNT(*) FROM todo_entries WHERE category = c.id) AS amount FROM categories AS c UNION ALL SELECT NULL, NULL, NULL, (SELECT COUNT(*) FROM todo_entries WHERE category IS NULL)',
|
||||||
|
@ -663,8 +669,14 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
Iterable<TableInfo<Table, dynamic>> get allTables =>
|
Iterable<TableInfo<Table, dynamic>> get allTables =>
|
||||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
@override
|
@override
|
||||||
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||||
[categories, todoEntries, textEntries, todosInsert];
|
categories,
|
||||||
|
todoEntries,
|
||||||
|
textEntries,
|
||||||
|
todosInsert,
|
||||||
|
todosDelete,
|
||||||
|
todosUpdate
|
||||||
|
];
|
||||||
@override
|
@override
|
||||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
|
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
|
||||||
[
|
[
|
||||||
|
@ -675,6 +687,20 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
TableUpdate('text_entries', kind: UpdateKind.insert),
|
TableUpdate('text_entries', kind: UpdateKind.insert),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
WritePropagation(
|
||||||
|
on: TableUpdateQuery.onTableName('todo_entries',
|
||||||
|
limitUpdateKind: UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
TableUpdate('text_entries', kind: UpdateKind.insert),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
WritePropagation(
|
||||||
|
on: TableUpdateQuery.onTableName('todo_entries',
|
||||||
|
limitUpdateKind: UpdateKind.update),
|
||||||
|
result: [
|
||||||
|
TableUpdate('text_entries', kind: UpdateKind.insert),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,14 @@ CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN
|
||||||
INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);
|
INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
-- todo: Investigate why these two triggers are causing problems
|
CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN
|
||||||
/*
|
INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', old.id, old.description);
|
||||||
CREATE TRIGGER todos_delete AFTER INSERT ON todo_entries BEGIN
|
|
||||||
INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', new.id, new.description);
|
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TRIGGER todos_update AFTER INSERT ON todo_entries BEGIN
|
CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN
|
||||||
INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', new.id, new.description);
|
INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', new.id, new.description);
|
||||||
INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);
|
INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);
|
||||||
END;
|
END;
|
||||||
*/
|
|
||||||
|
|
||||||
-- Queries can also be defined here, they're then added as methods to the database.
|
-- Queries can also be defined here, they're then added as methods to the database.
|
||||||
_categoriesWithCount: SELECT c.*,
|
_categoriesWithCount: SELECT c.*,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_dev/api/migrations.dart';
|
import 'package:drift_dev/api/migrations.dart';
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
|
import 'schema_v3.dart' as v3;
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
|
@ -11,10 +12,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case 2:
|
case 2:
|
||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
|
case 3:
|
||||||
|
return v3.DatabaseAtV3(db);
|
||||||
case 1:
|
case 1:
|
||||||
return v1.DatabaseAtV1(db);
|
return v1.DatabaseAtV1(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, const {2, 1});
|
throw MissingSchemaException(version, const {2, 3, 1});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,13 +78,18 @@ class TodoEntries extends Table with TableInfo {
|
||||||
bool get dontWriteConstraints => false;
|
bool get dontWriteConstraints => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class text_entriesTable extends Table with TableInfo, VirtualTableInfo {
|
class TextEntries extends Table with TableInfo, VirtualTableInfo {
|
||||||
@override
|
@override
|
||||||
final GeneratedDatabase attachedDatabase;
|
final GeneratedDatabase attachedDatabase;
|
||||||
final String? _alias;
|
final String? _alias;
|
||||||
text_entriesTable(this.attachedDatabase, [this._alias]);
|
TextEntries(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<String> description = GeneratedColumn<String>(
|
||||||
|
'description', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: '');
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [];
|
List<GeneratedColumn> get $columns => [description];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? 'text_entries';
|
String get aliasedName => _alias ?? 'text_entries';
|
||||||
@override
|
@override
|
||||||
|
@ -97,8 +102,8 @@ class text_entriesTable extends Table with TableInfo, VirtualTableInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
text_entriesTable createAlias(String alias) {
|
TextEntries createAlias(String alias) {
|
||||||
return text_entriesTable(attachedDatabase, alias);
|
return TextEntries(attachedDatabase, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -113,12 +118,13 @@ class DatabaseAtV1 extends GeneratedDatabase {
|
||||||
DatabaseAtV1.connect(DatabaseConnection c) : super.connect(c);
|
DatabaseAtV1.connect(DatabaseConnection c) : super.connect(c);
|
||||||
late final Categories categories = Categories(this);
|
late final Categories categories = Categories(this);
|
||||||
late final TodoEntries todoEntries = TodoEntries(this);
|
late final TodoEntries todoEntries = TodoEntries(this);
|
||||||
late final text_entriesTable textEntries = text_entriesTable(this);
|
late final TextEntries textEntries = TextEntries(this);
|
||||||
late final Trigger todosInsert = Trigger(
|
late final Trigger todosInsert = Trigger(
|
||||||
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;',
|
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;',
|
||||||
'todos_insert');
|
'todos_insert');
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
Iterable<TableInfo<Table, dynamic>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
@override
|
@override
|
||||||
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
||||||
[categories, todoEntries, textEntries, todosInsert];
|
[categories, todoEntries, textEntries, todosInsert];
|
||||||
|
|
|
@ -81,13 +81,18 @@ class TodoEntries extends Table with TableInfo {
|
||||||
bool get dontWriteConstraints => false;
|
bool get dontWriteConstraints => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class text_entriesTable extends Table with TableInfo, VirtualTableInfo {
|
class TextEntries extends Table with TableInfo, VirtualTableInfo {
|
||||||
@override
|
@override
|
||||||
final GeneratedDatabase attachedDatabase;
|
final GeneratedDatabase attachedDatabase;
|
||||||
final String? _alias;
|
final String? _alias;
|
||||||
text_entriesTable(this.attachedDatabase, [this._alias]);
|
TextEntries(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<String> description = GeneratedColumn<String>(
|
||||||
|
'description', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: '');
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [];
|
List<GeneratedColumn> get $columns => [description];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? 'text_entries';
|
String get aliasedName => _alias ?? 'text_entries';
|
||||||
@override
|
@override
|
||||||
|
@ -100,8 +105,8 @@ class text_entriesTable extends Table with TableInfo, VirtualTableInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
text_entriesTable createAlias(String alias) {
|
TextEntries createAlias(String alias) {
|
||||||
return text_entriesTable(attachedDatabase, alias);
|
return TextEntries(attachedDatabase, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -116,12 +121,13 @@ class DatabaseAtV2 extends GeneratedDatabase {
|
||||||
DatabaseAtV2.connect(DatabaseConnection c) : super.connect(c);
|
DatabaseAtV2.connect(DatabaseConnection c) : super.connect(c);
|
||||||
late final Categories categories = Categories(this);
|
late final Categories categories = Categories(this);
|
||||||
late final TodoEntries todoEntries = TodoEntries(this);
|
late final TodoEntries todoEntries = TodoEntries(this);
|
||||||
late final text_entriesTable textEntries = text_entriesTable(this);
|
late final TextEntries textEntries = TextEntries(this);
|
||||||
late final Trigger todosInsert = Trigger(
|
late final Trigger todosInsert = Trigger(
|
||||||
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;',
|
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;',
|
||||||
'todos_insert');
|
'todos_insert');
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
Iterable<TableInfo<Table, dynamic>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
@override
|
@override
|
||||||
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
||||||
[categories, todoEntries, textEntries, todosInsert];
|
[categories, todoEntries, textEntries, todosInsert];
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||||
|
//@dart=2.12
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
class Categories extends Table with TableInfo {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
Categories(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
|
||||||
|
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||||
|
'name', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
late final GeneratedColumn<int> color = GeneratedColumn<int>(
|
||||||
|
'color', aliasedName, false,
|
||||||
|
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [id, name, color];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? 'categories';
|
||||||
|
@override
|
||||||
|
String get actualTableName => 'categories';
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Categories createAlias(String alias) {
|
||||||
|
return Categories(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoEntries extends Table with TableInfo {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
TodoEntries(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
|
||||||
|
late final GeneratedColumn<String> description = GeneratedColumn<String>(
|
||||||
|
'description', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
late final GeneratedColumn<int> category = GeneratedColumn<int>(
|
||||||
|
'category', aliasedName, true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: 'REFERENCES categories (id)');
|
||||||
|
late final GeneratedColumn<DateTime> dueDate = GeneratedColumn<DateTime>(
|
||||||
|
'due_date', aliasedName, true,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [id, description, category, dueDate];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? 'todo_entries';
|
||||||
|
@override
|
||||||
|
String get actualTableName => 'todo_entries';
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TodoEntries createAlias(String alias) {
|
||||||
|
return TodoEntries(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextEntries extends Table with TableInfo, VirtualTableInfo {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
TextEntries(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<String> description = GeneratedColumn<String>(
|
||||||
|
'description', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: '');
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [description];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? 'text_entries';
|
||||||
|
@override
|
||||||
|
String get actualTableName => 'text_entries';
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||||
|
@override
|
||||||
|
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEntries createAlias(String alias) {
|
||||||
|
return TextEntries(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => true;
|
||||||
|
@override
|
||||||
|
String get moduleAndArgs =>
|
||||||
|
'fts5(description, content=todo_entries, content_rowid=id)';
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseAtV3 extends GeneratedDatabase {
|
||||||
|
DatabaseAtV3(QueryExecutor e) : super(e);
|
||||||
|
DatabaseAtV3.connect(DatabaseConnection c) : super.connect(c);
|
||||||
|
late final Categories categories = Categories(this);
|
||||||
|
late final TodoEntries todoEntries = TodoEntries(this);
|
||||||
|
late final TextEntries textEntries = TextEntries(this);
|
||||||
|
late final Trigger todosInsert = Trigger(
|
||||||
|
'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END',
|
||||||
|
'todos_insert');
|
||||||
|
late final Trigger todosDelete = Trigger(
|
||||||
|
'CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', old.id, old.description);END',
|
||||||
|
'todos_delete');
|
||||||
|
late final Trigger todosUpdate = Trigger(
|
||||||
|
'CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', new.id, new.description);INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END',
|
||||||
|
'todos_update');
|
||||||
|
@override
|
||||||
|
Iterable<TableInfo<Table, dynamic>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
|
@override
|
||||||
|
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||||
|
categories,
|
||||||
|
todoEntries,
|
||||||
|
textEntries,
|
||||||
|
todosInsert,
|
||||||
|
todosDelete,
|
||||||
|
todosUpdate
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 3;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:app/database/database.dart';
|
import 'package:app/database/database.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_dev/api/migrations.dart';
|
import 'package:drift_dev/api/migrations.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
@ -13,6 +14,60 @@ void main() {
|
||||||
verifier = SchemaVerifier(GeneratedHelper());
|
verifier = SchemaVerifier(GeneratedHelper());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('schema integrity is kept', () {
|
||||||
|
const currentVersion = 3;
|
||||||
|
|
||||||
|
// This loop tests all possible schema upgrades. It uses drift APIs to
|
||||||
|
// ensure that the schema is in the expected format after an upgrade, but
|
||||||
|
// simple tests like these can't ensure that your migration doesn't loose
|
||||||
|
// data.
|
||||||
|
for (var start = 1; start < currentVersion; start++) {
|
||||||
|
group('from v$start', () {
|
||||||
|
for (var target = start + 1; target <= currentVersion; target++) {
|
||||||
|
test('to v$target', () async {
|
||||||
|
// Use startAt() to obtain a database connection with all tables
|
||||||
|
// from the old schema set up.
|
||||||
|
final connection = await verifier.startAt(start);
|
||||||
|
final db = AppDatabase.forTesting(connection);
|
||||||
|
addTearDown(db.close);
|
||||||
|
|
||||||
|
// Use this to run a migration and then validate that the database
|
||||||
|
// has the expected schema.
|
||||||
|
await verifier.migrateAndValidate(db, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For specific schema upgrades, you can also write manual tests to ensure
|
||||||
|
// that running the migration does not loose data.
|
||||||
|
test('upgrading from v1 to v2 does not loose data', () async {
|
||||||
|
// Use startAt(1) to obtain a usable database
|
||||||
|
final connection = await verifier.schemaAt(1);
|
||||||
|
connection.rawDatabase.execute(
|
||||||
|
'INSERT INTO todo_entries (description) VALUES (?)',
|
||||||
|
['My manually added entry'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final db = AppDatabase.forTesting(connection.newConnection());
|
||||||
|
addTearDown(db.close);
|
||||||
|
await verifier.migrateAndValidate(db, 2);
|
||||||
|
|
||||||
|
// Make sure that the row is still there after migrating
|
||||||
|
expect(
|
||||||
|
db.todoEntries.select().get(),
|
||||||
|
completion(
|
||||||
|
[
|
||||||
|
const TodoEntry(
|
||||||
|
id: 1,
|
||||||
|
description: 'My manually added entry',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('upgrade from v1 to v2', () async {
|
test('upgrade from v1 to v2', () async {
|
||||||
// Use startAt(1) to obtain a database connection with all tables
|
// Use startAt(1) to obtain a database connection with all tables
|
||||||
// from the v1 schema.
|
// from the v1 schema.
|
||||||
|
|
|
@ -739,7 +739,7 @@ class Parser {
|
||||||
Literal? _literalOrNull() {
|
Literal? _literalOrNull() {
|
||||||
final token = _peek;
|
final token = _peek;
|
||||||
|
|
||||||
Literal? _parseInner() {
|
Literal? parseInner() {
|
||||||
if (_check(TokenType.numberLiteral)) {
|
if (_check(TokenType.numberLiteral)) {
|
||||||
return _numericLiteral();
|
return _numericLiteral();
|
||||||
}
|
}
|
||||||
|
@ -770,7 +770,7 @@ class Parser {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final literal = _parseInner();
|
final literal = parseInner();
|
||||||
literal?.setSpan(token, token);
|
literal?.setSpan(token, token);
|
||||||
return literal;
|
return literal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -319,7 +319,7 @@ class Scanner {
|
||||||
|
|
||||||
/// Returns true without advancing if the next char is a digit. Returns
|
/// Returns true without advancing if the next char is a digit. Returns
|
||||||
/// false and logs an error with the message otherwise.
|
/// false and logs an error with the message otherwise.
|
||||||
bool _requireDigit(String message) {
|
bool requireDigit(String message) {
|
||||||
final noDigit = _isAtEnd || !isDigit(_peek());
|
final noDigit = _isAtEnd || !isDigit(_peek());
|
||||||
if (noDigit) {
|
if (noDigit) {
|
||||||
errors.add(TokenizerError(message, _currentLocation));
|
errors.add(TokenizerError(message, _currentLocation));
|
||||||
|
@ -335,7 +335,7 @@ class Scanner {
|
||||||
if (firstChar == $dot) {
|
if (firstChar == $dot) {
|
||||||
// started with a decimal point. the next char has to be numeric
|
// started with a decimal point. the next char has to be numeric
|
||||||
hasDecimal = true;
|
hasDecimal = true;
|
||||||
if (_requireDigit('Expected a digit after the decimal dot')) {
|
if (requireDigit('Expected a digit after the decimal dot')) {
|
||||||
afterDecimal = consumeDigits();
|
afterDecimal = consumeDigits();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -380,7 +380,7 @@ class Scanner {
|
||||||
parsedExponent = int.parse(exponent);
|
parsedExponent = int.parse(exponent);
|
||||||
} else {
|
} else {
|
||||||
if (char == $plus || char == $minus) {
|
if (char == $plus || char == $minus) {
|
||||||
_requireDigit('Expected digits for the exponent');
|
requireDigit('Expected digits for the exponent');
|
||||||
final exponent = consumeDigits();
|
final exponent = consumeDigits();
|
||||||
|
|
||||||
parsedExponent =
|
parsedExponent =
|
||||||
|
|
|
@ -9,84 +9,84 @@ void main() {
|
||||||
..registerTable(demoTable)
|
..registerTable(demoTable)
|
||||||
..registerTable(anotherTable);
|
..registerTable(anotherTable);
|
||||||
|
|
||||||
TypeResolver _obtainResolver(String sql, {AnalyzeStatementOptions? options}) {
|
TypeResolver obtainResolver(String sql, {AnalyzeStatementOptions? options}) {
|
||||||
final context = engine.analyze(sql, stmtOptions: options);
|
final context = engine.analyze(sql, stmtOptions: options);
|
||||||
return TypeResolver(TypeInferenceSession(context))..run(context.root);
|
return TypeResolver(TypeInferenceSession(context))..run(context.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedType? _resolveFirstVariable(String sql,
|
ResolvedType? resolveFirstVariable(String sql,
|
||||||
{AnalyzeStatementOptions? options}) {
|
{AnalyzeStatementOptions? options}) {
|
||||||
final resolver = _obtainResolver(sql, options: options);
|
final resolver = obtainResolver(sql, options: options);
|
||||||
final session = resolver.session;
|
final session = resolver.session;
|
||||||
final variable =
|
final variable =
|
||||||
session.context.root.allDescendants.whereType<Variable>().first;
|
session.context.root.allDescendants.whereType<Variable>().first;
|
||||||
return session.typeOf(variable);
|
return session.typeOf(variable);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedType? _resolveResultColumn(String sql) {
|
ResolvedType? resolveResultColumn(String sql) {
|
||||||
final resolver = _obtainResolver(sql);
|
final resolver = obtainResolver(sql);
|
||||||
final session = resolver.session;
|
final session = resolver.session;
|
||||||
final stmt = session.context.root as SelectStatement;
|
final stmt = session.context.root as SelectStatement;
|
||||||
return session.typeOf(stmt.resolvedColumns!.single);
|
return session.typeOf(stmt.resolvedColumns!.single);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('resolves literals', () {
|
test('resolves literals', () {
|
||||||
expect(_resolveResultColumn('SELECT NULL'),
|
expect(resolveResultColumn('SELECT NULL'),
|
||||||
const ResolvedType(type: BasicType.nullType, nullable: true));
|
const ResolvedType(type: BasicType.nullType, nullable: true));
|
||||||
|
|
||||||
expect(_resolveResultColumn('SELECT TRUE'), const ResolvedType.bool());
|
expect(resolveResultColumn('SELECT TRUE'), const ResolvedType.bool());
|
||||||
expect(_resolveResultColumn("SELECT x''"),
|
expect(resolveResultColumn("SELECT x''"),
|
||||||
const ResolvedType(type: BasicType.blob));
|
const ResolvedType(type: BasicType.blob));
|
||||||
expect(_resolveResultColumn("SELECT ''"),
|
expect(resolveResultColumn("SELECT ''"),
|
||||||
const ResolvedType(type: BasicType.text));
|
const ResolvedType(type: BasicType.text));
|
||||||
expect(_resolveResultColumn('SELECT 3'),
|
expect(resolveResultColumn('SELECT 3'),
|
||||||
const ResolvedType(type: BasicType.int));
|
const ResolvedType(type: BasicType.int));
|
||||||
expect(_resolveResultColumn('SELECT 3.5'),
|
expect(resolveResultColumn('SELECT 3.5'),
|
||||||
const ResolvedType(type: BasicType.real));
|
const ResolvedType(type: BasicType.real));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers boolean type in where conditions', () {
|
test('infers boolean type in where conditions', () {
|
||||||
expect(_resolveFirstVariable('SELECT * FROM demo WHERE :foo'),
|
expect(resolveFirstVariable('SELECT * FROM demo WHERE :foo'),
|
||||||
const ResolvedType.bool());
|
const ResolvedType.bool());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not infer boolean for grandchildren of where clause', () {
|
test('does not infer boolean for grandchildren of where clause', () {
|
||||||
expect(_resolveFirstVariable("SELECT * FROM demo WHERE 'foo' = :foo"),
|
expect(resolveFirstVariable("SELECT * FROM demo WHERE 'foo' = :foo"),
|
||||||
const ResolvedType(type: BasicType.text));
|
const ResolvedType(type: BasicType.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers boolean type in a join ON clause', () {
|
test('infers boolean type in a join ON clause', () {
|
||||||
expect(
|
expect(
|
||||||
_resolveFirstVariable('SELECT * FROM demo JOIN tbl ON :foo'),
|
resolveFirstVariable('SELECT * FROM demo JOIN tbl ON :foo'),
|
||||||
const ResolvedType.bool(),
|
const ResolvedType.bool(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers type in a string concatenation', () {
|
test('infers type in a string concatenation', () {
|
||||||
expect(_resolveFirstVariable("SELECT '' || :foo"),
|
expect(resolveFirstVariable("SELECT '' || :foo"),
|
||||||
const ResolvedType(type: BasicType.text));
|
const ResolvedType(type: BasicType.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolves arithmetic expressions', () {
|
test('resolves arithmetic expressions', () {
|
||||||
expect(_resolveFirstVariable('SELECT ((3 + 4) * 5) = ?'),
|
expect(resolveFirstVariable('SELECT ((3 + 4) * 5) = ?'),
|
||||||
const ResolvedType(type: BasicType.int));
|
const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
group('cast expressions', () {
|
group('cast expressions', () {
|
||||||
test('resolve to type argument', () {
|
test('resolve to type argument', () {
|
||||||
expect(_resolveResultColumn('SELECT CAST(3+4 AS TEXT)'),
|
expect(resolveResultColumn('SELECT CAST(3+4 AS TEXT)'),
|
||||||
const ResolvedType(type: BasicType.text));
|
const ResolvedType(type: BasicType.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow anything as their operand', () {
|
test('allow anything as their operand', () {
|
||||||
expect(_resolveFirstVariable('SELECT CAST(? AS TEXT)'), null);
|
expect(resolveFirstVariable('SELECT CAST(? AS TEXT)'), null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('types in insert statements', () {
|
group('types in insert statements', () {
|
||||||
test('for VALUES', () {
|
test('for VALUES', () {
|
||||||
final resolver =
|
final resolver =
|
||||||
_obtainResolver('INSERT INTO demo VALUES (:id, :content);');
|
obtainResolver('INSERT INTO demo VALUES (:id, :content);');
|
||||||
final root = resolver.session.context.root;
|
final root = resolver.session.context.root;
|
||||||
final variables = root.allDescendants.whereType<Variable>();
|
final variables = root.allDescendants.whereType<Variable>();
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('for SELECT', () {
|
test('for SELECT', () {
|
||||||
final resolver = _obtainResolver('INSERT INTO demo SELECT :id, :content');
|
final resolver = obtainResolver('INSERT INTO demo SELECT :id, :content');
|
||||||
final root = resolver.session.context.root;
|
final root = resolver.session.context.root;
|
||||||
final variables = root.allDescendants.whereType<Variable>();
|
final variables = root.allDescendants.whereType<Variable>();
|
||||||
|
|
||||||
|
@ -112,14 +112,14 @@ void main() {
|
||||||
|
|
||||||
test('recognizes that variables are the same', () {
|
test('recognizes that variables are the same', () {
|
||||||
// semantically, :var and ?1 are the same variable
|
// semantically, :var and ?1 are the same variable
|
||||||
final type = _resolveFirstVariable('SELECT :var WHERE ?1');
|
final type = resolveFirstVariable('SELECT :var WHERE ?1');
|
||||||
expect(type, const ResolvedType.bool());
|
expect(type, const ResolvedType.bool());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('respects variable types set from options', () {
|
test('respects variable types set from options', () {
|
||||||
const type = ResolvedType(type: BasicType.text);
|
const type = ResolvedType(type: BasicType.text);
|
||||||
// should resolve to string, even though it would be a boolean normally
|
// should resolve to string, even though it would be a boolean normally
|
||||||
final found = _resolveFirstVariable(
|
final found = resolveFirstVariable(
|
||||||
'SELECT * FROM demo WHERE ?',
|
'SELECT * FROM demo WHERE ?',
|
||||||
options: const AnalyzeStatementOptions(indexedVariableTypes: {1: type}),
|
options: const AnalyzeStatementOptions(indexedVariableTypes: {1: type}),
|
||||||
);
|
);
|
||||||
|
@ -130,25 +130,25 @@ void main() {
|
||||||
test('handles LIMIT clauses', () {
|
test('handles LIMIT clauses', () {
|
||||||
const int = ResolvedType(type: BasicType.int);
|
const int = ResolvedType(type: BasicType.int);
|
||||||
|
|
||||||
final type = _resolveFirstVariable('SELECT 0 LIMIT ?');
|
final type = resolveFirstVariable('SELECT 0 LIMIT ?');
|
||||||
expect(type, int);
|
expect(type, int);
|
||||||
|
|
||||||
final offsetType = _resolveFirstVariable('SELECT 0 LIMIT 1, ?');
|
final offsetType = resolveFirstVariable('SELECT 0 LIMIT 1, ?');
|
||||||
expect(offsetType, int);
|
expect(offsetType, int);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles string matching expressions', () {
|
test('handles string matching expressions', () {
|
||||||
final type =
|
final type =
|
||||||
_resolveFirstVariable('SELECT * FROM demo WHERE content LIKE ?');
|
resolveFirstVariable('SELECT * FROM demo WHERE content LIKE ?');
|
||||||
expect(type, const ResolvedType(type: BasicType.text));
|
expect(type, const ResolvedType(type: BasicType.text));
|
||||||
|
|
||||||
final escapedType = _resolveFirstVariable(
|
final escapedType = resolveFirstVariable(
|
||||||
"SELECT * FROM demo WHERE content LIKE 'foo' ESCAPE ?");
|
"SELECT * FROM demo WHERE content LIKE 'foo' ESCAPE ?");
|
||||||
expect(escapedType, const ResolvedType(type: BasicType.text));
|
expect(escapedType, const ResolvedType(type: BasicType.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles nth_value', () {
|
test('handles nth_value', () {
|
||||||
final resolver = _obtainResolver("SELECT nth_value('string', ?1) = ?2");
|
final resolver = obtainResolver("SELECT nth_value('string', ?1) = ?2");
|
||||||
final variables = resolver.session.context.root.allDescendants
|
final variables = resolver.session.context.root.allDescendants
|
||||||
.whereType<Variable>()
|
.whereType<Variable>()
|
||||||
.iterator;
|
.iterator;
|
||||||
|
@ -166,44 +166,44 @@ void main() {
|
||||||
|
|
||||||
group('case expressions', () {
|
group('case expressions', () {
|
||||||
test('infers base clause from when', () {
|
test('infers base clause from when', () {
|
||||||
final type = _resolveFirstVariable("SELECT CASE ? WHEN 1 THEN 'two' END");
|
final type = resolveFirstVariable("SELECT CASE ? WHEN 1 THEN 'two' END");
|
||||||
expect(type, const ResolvedType(type: BasicType.int));
|
expect(type, const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers when condition from base', () {
|
test('infers when condition from base', () {
|
||||||
final type = _resolveFirstVariable("SELECT CASE 1 WHEN ? THEN 'two' END");
|
final type = resolveFirstVariable("SELECT CASE 1 WHEN ? THEN 'two' END");
|
||||||
expect(type, const ResolvedType(type: BasicType.int));
|
expect(type, const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers when conditions as boolean when no base is set', () {
|
test('infers when conditions as boolean when no base is set', () {
|
||||||
final type = _resolveFirstVariable("SELECT CASE WHEN ? THEN 'two' END;");
|
final type = resolveFirstVariable("SELECT CASE WHEN ? THEN 'two' END;");
|
||||||
expect(type, const ResolvedType.bool());
|
expect(type, const ResolvedType.bool());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers type of whole when expression', () {
|
test('infers type of whole when expression', () {
|
||||||
final type = _resolveResultColumn("SELECT CASE WHEN false THEN 'one' "
|
final type = resolveResultColumn("SELECT CASE WHEN false THEN 'one' "
|
||||||
"WHEN true THEN 'two' ELSE 'three' END;");
|
"WHEN true THEN 'two' ELSE 'three' END;");
|
||||||
expect(type, const ResolvedType(type: BasicType.text));
|
expect(type, const ResolvedType(type: BasicType.text));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can select columns', () {
|
test('can select columns', () {
|
||||||
final type = _resolveResultColumn('SELECT id FROM demo;');
|
final type = resolveResultColumn('SELECT id FROM demo;');
|
||||||
expect(type, const ResolvedType(type: BasicType.int));
|
expect(type, const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers type of EXISTS expressions', () {
|
test('infers type of EXISTS expressions', () {
|
||||||
final type = _resolveResultColumn('SELECT EXISTS(SELECT * FROM demo);');
|
final type = resolveResultColumn('SELECT EXISTS(SELECT * FROM demo);');
|
||||||
expect(type, const ResolvedType.bool());
|
expect(type, const ResolvedType.bool());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolves subqueries', () {
|
test('resolves subqueries', () {
|
||||||
final type = _resolveResultColumn('SELECT (SELECT COUNT(*) FROM demo);');
|
final type = resolveResultColumn('SELECT (SELECT COUNT(*) FROM demo);');
|
||||||
expect(type, const ResolvedType(type: BasicType.int));
|
expect(type, const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers types for dart placeholders', () {
|
test('infers types for dart placeholders', () {
|
||||||
final resolver = _obtainResolver(r'SELECT * FROM demo WHERE $pred');
|
final resolver = obtainResolver(r'SELECT * FROM demo WHERE $pred');
|
||||||
final type = resolver.session.typeOf(resolver
|
final type = resolver.session.typeOf(resolver
|
||||||
.session.context.root.allDescendants
|
.session.context.root.allDescendants
|
||||||
.firstWhere((e) => e is DartExpressionPlaceholder)
|
.firstWhere((e) => e is DartExpressionPlaceholder)
|
||||||
|
@ -214,7 +214,7 @@ void main() {
|
||||||
|
|
||||||
test('supports unions', () {
|
test('supports unions', () {
|
||||||
void check(String sql) {
|
void check(String sql) {
|
||||||
final resolver = _obtainResolver(sql);
|
final resolver = obtainResolver(sql);
|
||||||
final column = (resolver.session.context.root as CompoundSelectStatement)
|
final column = (resolver.session.context.root as CompoundSelectStatement)
|
||||||
.resolvedColumns!
|
.resolvedColumns!
|
||||||
.single;
|
.single;
|
||||||
|
@ -228,7 +228,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles recursive CTEs', () {
|
test('handles recursive CTEs', () {
|
||||||
final type = _resolveResultColumn('''
|
final type = resolveResultColumn('''
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
cnt(x) AS (
|
cnt(x) AS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
|
@ -243,19 +243,18 @@ WITH RECURSIVE
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles set components in updates', () {
|
test('handles set components in updates', () {
|
||||||
final type = _resolveFirstVariable('UPDATE demo SET id = ?');
|
final type = resolveFirstVariable('UPDATE demo SET id = ?');
|
||||||
expect(type, const ResolvedType(type: BasicType.int));
|
expect(type, const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers offsets in frame specs', () {
|
test('infers offsets in frame specs', () {
|
||||||
final type =
|
final type = resolveFirstVariable('SELECT SUM(id) OVER (ROWS ? PRECEDING)');
|
||||||
_resolveFirstVariable('SELECT SUM(id) OVER (ROWS ? PRECEDING)');
|
|
||||||
expect(type, const ResolvedType(type: BasicType.int));
|
expect(type, const ResolvedType(type: BasicType.int));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolves type hints from between expressions', () {
|
test('resolves type hints from between expressions', () {
|
||||||
const dateTime = ResolvedType(type: BasicType.int, hint: IsDateTime());
|
const dateTime = ResolvedType(type: BasicType.int, hint: IsDateTime());
|
||||||
final session = _obtainResolver(
|
final session = obtainResolver(
|
||||||
'SELECT 1 WHERE :date BETWEEN :start AND :end',
|
'SELECT 1 WHERE :date BETWEEN :start AND :end',
|
||||||
options: const AnalyzeStatementOptions(
|
options: const AnalyzeStatementOptions(
|
||||||
namedVariableTypes: {':date': dateTime},
|
namedVariableTypes: {':date': dateTime},
|
||||||
|
@ -276,23 +275,23 @@ WITH RECURSIVE
|
||||||
|
|
||||||
group('IS IN expressions', () {
|
group('IS IN expressions', () {
|
||||||
test('infer the variable as an array type', () {
|
test('infer the variable as an array type', () {
|
||||||
final type = _resolveFirstVariable('SELECT 3 IN ?');
|
final type = resolveFirstVariable('SELECT 3 IN ?');
|
||||||
expect(type, const ResolvedType(type: BasicType.int, isArray: true));
|
expect(type, const ResolvedType(type: BasicType.int, isArray: true));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not infer the variable as an array when in a tuple', () {
|
test('does not infer the variable as an array when in a tuple', () {
|
||||||
final type = _resolveFirstVariable('SELECT 3 IN (?)');
|
final type = resolveFirstVariable('SELECT 3 IN (?)');
|
||||||
expect(type, const ResolvedType(type: BasicType.int, isArray: false));
|
expect(type, const ResolvedType(type: BasicType.int, isArray: false));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('columns from LEFT OUTER joins are nullable', () {
|
test('columns from LEFT OUTER joins are nullable', () {
|
||||||
final resolver = _obtainResolver('''
|
final resolver = obtainResolver('''
|
||||||
WITH
|
WITH
|
||||||
sq_1 (one ) AS (SELECT 1),
|
sq_1 (one ) AS (SELECT 1),
|
||||||
sq_2 (two) AS (SELECT 2),
|
sq_2 (two) AS (SELECT 2),
|
||||||
sq_3 (three) AS (SELECT 3)
|
sq_3 (three) AS (SELECT 3)
|
||||||
|
|
||||||
SELECT one, two, three
|
SELECT one, two, three
|
||||||
FROM sq_1
|
FROM sq_1
|
||||||
LEFT JOIN sq_2
|
LEFT JOIN sq_2
|
||||||
|
|
Loading…
Reference in New Issue