mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
Merge branch 'stream-wait-for-close' into new-query-builder
This commit is contained in:
@@ -92,7 +92,6 @@ jobs:
|
||||
- run: dart analyze --fatal-infos --fatal-warnings
|
||||
# build, test and upload coverage
|
||||
- run: dart run build_runner build --delete-conflicting-outputs
|
||||
- run: dart run build_runner doctor
|
||||
- run: dart test --preset ci_unit_tests #-x background_isolate --coverage=coverage
|
||||
env:
|
||||
DID_RUN_BUILD_RUNNER: "1"
|
||||
|
||||
@@ -95,16 +95,24 @@ class MyDatabase extends _$MyDatabase {
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onUpgrade: stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
await m.createTable(schema.groups);
|
||||
},
|
||||
),
|
||||
onUpgrade: _schemaUpgrade,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension Migrations on GeneratedDatabase { // (1)
|
||||
OnUpgrade get _schemaUpgrade => stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
await m.createTable(schema.groups);
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
1. Extracting the `stepByStep` call into a static field or method ensures that you're not accidentally
|
||||
referring to the current database schema (via a getter on the database class).
|
||||
This ensures that each step brings the database into the correct snapshot.
|
||||
|
||||
See the [example](https://github.com/simolus3/drift/tree/develop/examples/migrations_example) in the drift repository for a complete example of how to use the `make-migrations` command.
|
||||
|
||||
### Switching to `make-migrations`
|
||||
|
||||
@@ -301,6 +301,13 @@ extension InsertStatementWithDatabase<
|
||||
/// violations. If you have further uniqueness constraints, please use the
|
||||
/// general [insert] method with a [DoUpdate] including those columns in its
|
||||
/// [DoUpdate.target].
|
||||
///
|
||||
/// Like the other [insert] methods, this returns the rowid of the last
|
||||
/// insert. When the insert was turned into an update due to a conflicting
|
||||
/// row, this behavior is somewhat confusing because the id of an (likely
|
||||
/// unrelated) insert is returned instead.
|
||||
/// Use [insertReturning] with an `onConflict` clause of
|
||||
/// `DoUpdate((_) => entity)` to reliably get the written entity instead.
|
||||
Future<int> insertOnConflictUpdate(Insertable<Row> entity) {
|
||||
return insert(entity, onConflict: DoUpdate((_) => entity));
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ base class LocalStreamQueryStore implements StreamQueryStore {
|
||||
_isShuttingDown = true;
|
||||
|
||||
for (final stream in _activeKeyStreams.values) {
|
||||
stream.close();
|
||||
await stream.close();
|
||||
}
|
||||
// awaiting this is fine - the stream is never exposed to users and we don't
|
||||
// pause any subscriptions on it.
|
||||
@@ -155,7 +155,7 @@ class QueryStream<Rows extends Object> {
|
||||
final queryListener = _QueryStreamListener(listener);
|
||||
|
||||
if (_isClosed) {
|
||||
listener.closeSync();
|
||||
listener.close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,12 +300,13 @@ class QueryStream<Rows extends Object> {
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
Future<void> close() async {
|
||||
_isClosed = true;
|
||||
for (final listener in _listeners) {
|
||||
listener.controller.close();
|
||||
}
|
||||
|
||||
final listenersDone = Future.wait(
|
||||
[for (final listener in _listeners) listener.controller.close()]);
|
||||
_listeners.clear();
|
||||
await listenersDone;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ dependencies:
|
||||
|
||||
dev_dependencies:
|
||||
archive: ^4.0.2
|
||||
analyzer: ^7.0.0
|
||||
analyzer: '>=7.0.0 <9.0.0'
|
||||
build_test: ^3.2.0-0
|
||||
build_runner_core: ^9.2.0-0
|
||||
build_verify: ^3.1.0
|
||||
|
||||
@@ -274,6 +274,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
stream.cancel();
|
||||
await db.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,21 @@ void main() {
|
||||
|
||||
subscription.resume();
|
||||
await subscription.cancel();
|
||||
}, skip: 'testing out awaited streams');
|
||||
|
||||
test('closing database waits for streams', () async {
|
||||
final stream = db.select(db.users).watch();
|
||||
final subscription = stream.listen((_) {})..pause();
|
||||
|
||||
var closed = false;
|
||||
db.close().then((_) => closed = true);
|
||||
await pumpEventQueue();
|
||||
expect(closed, isFalse);
|
||||
|
||||
subscription.resume();
|
||||
await subscription.cancel();
|
||||
await pumpEventQueue();
|
||||
expect(closed, isTrue);
|
||||
});
|
||||
|
||||
group('stream keys', () {
|
||||
|
||||
@@ -92,6 +92,7 @@ void main() {
|
||||
|
||||
test('can be used in a query stream', () async {
|
||||
final stream = StreamQueue(db.readView().watch());
|
||||
addTearDown(stream.cancel);
|
||||
const entry = Config(
|
||||
configKey: 'another_key',
|
||||
configValue: DriftAny('value'),
|
||||
|
||||
@@ -12,6 +12,7 @@ void main() {
|
||||
addTearDown(db.close);
|
||||
|
||||
final query = StreamQueue(db.select(db.myView).watch());
|
||||
addTearDown(query.cancel);
|
||||
await expectLater(query, emits(isEmpty));
|
||||
|
||||
await db.into(db.config).insert(ConfigCompanion.insert(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
## 2.28.2-dev
|
||||
## 2.28.2
|
||||
|
||||
- Fix generating code for `typedef`s with different parameters than the type they point to.
|
||||
- Support `analyzer` version 8.x
|
||||
- When no other option is set, assume SQLite version 3.38.0 when analyzing drift files
|
||||
(used to be 3.31.0).
|
||||
|
||||
## 2.28.1
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ sealed class RegisteredDriftDialect {
|
||||
|
||||
@JsonSerializable()
|
||||
final class DriftSqliteDialect implements RegisteredDriftDialect {
|
||||
static const _defaultSqliteVersion = SqliteVersion.v3(34);
|
||||
static const _defaultSqliteVersion = SqliteVersion.v3(38);
|
||||
|
||||
@override
|
||||
String get name => 'sqlite';
|
||||
|
||||
@@ -33,67 +33,9 @@ class FileAnalyzer {
|
||||
for (final elementAnalysis in state.analysis.values) {
|
||||
final element = elementAnalysis.result;
|
||||
|
||||
final queries = <String, SqlQuery>{};
|
||||
|
||||
final imports = <FileState>[];
|
||||
|
||||
if (element is BaseDriftAccessor) {
|
||||
for (final include in element.declaredIncludes) {
|
||||
final imported = await driver.resolveElements(driver.backend
|
||||
.resolveUri(element.declaration.sourceUri, include.toString()));
|
||||
|
||||
imports.add(imported);
|
||||
}
|
||||
|
||||
final imported = driver.cache.crawlMulti(imports).toSet();
|
||||
for (final import in imported) {
|
||||
await driver.resolveElements(import.ownUri);
|
||||
}
|
||||
|
||||
final availableByDefault = <DriftSchemaElement>{
|
||||
...element.declaredTables,
|
||||
...element.declaredViews,
|
||||
};
|
||||
|
||||
// For indices added to tables via an annotation, the index should
|
||||
// also be available.
|
||||
for (final table in element.declaredTables) {
|
||||
final fileState = driver.cache.knownFiles[table.id.libraryUri]!;
|
||||
|
||||
for (final attachedIndex in table.attachedIndices) {
|
||||
final index = await driver.resolveElement(
|
||||
fileState, fileState.id(attachedIndex));
|
||||
|
||||
if (index is DriftIndex) {
|
||||
availableByDefault.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final availableElements = imported
|
||||
.expand((reachable) {
|
||||
final elementAnalysis = reachable.analysis.values;
|
||||
|
||||
return elementAnalysis.map((e) => e.result).where(
|
||||
(e) => e is DefinedSqlQuery || e is DriftSchemaElement);
|
||||
})
|
||||
.whereType<DriftElement>()
|
||||
.where((e) {
|
||||
// Exclude any private tables that do not reside in the same library
|
||||
// as the DriftDatabase.
|
||||
// Failure to exclude these, can generate dart code which references
|
||||
// classes that cannot be legally accessed - and will not compile.
|
||||
// Private classes residing in the same library are allowed, as
|
||||
// per dart language accessibility rules.
|
||||
if (e is DriftElementWithResultSet &&
|
||||
e.entityInfoName.startsWith(r'$_')) {
|
||||
return e.id.libraryUri == element.id.libraryUri;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.followedBy(availableByDefault)
|
||||
.transitiveClosureUnderReferences()
|
||||
.sortTopologicallyOrElse(driver.backend.log.severe);
|
||||
final (:availableElements, :availableByDefault, :imports) =
|
||||
await _resolveElementsAndImports(element);
|
||||
|
||||
// We will generate code for all available elements - even those only
|
||||
// reachable through imports. If that means we're pulling in a table
|
||||
@@ -117,8 +59,28 @@ class FileAnalyzer {
|
||||
'be included in this database: $names',
|
||||
);
|
||||
}
|
||||
|
||||
for (final dao in element.accessors) {
|
||||
final schema = (await _resolveElementsAndImports(dao))
|
||||
.availableElements
|
||||
.whereType<DriftSchemaElement>();
|
||||
|
||||
final onlyReferencedFromDao = schema
|
||||
.where((e) => !availableByDefault.contains(e))
|
||||
.map((e) => e.id.name)
|
||||
.toList();
|
||||
|
||||
if (onlyReferencedFromDao.isNotEmpty) {
|
||||
driver.backend.log.warning(
|
||||
"Dao ${dao.ownType} references tables that aren't available "
|
||||
'on the main database: $onlyReferencedFromDao. These '
|
||||
'must also be included in the main database.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final queries = <String, SqlQuery>{};
|
||||
for (final query in element.declaredQueries) {
|
||||
final engine = typeMapping.newEngineWithTables(availableElements);
|
||||
final context = engine.analyze(query.sql);
|
||||
@@ -211,6 +173,79 @@ class FileAnalyzer {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<
|
||||
({
|
||||
List<DriftElement> availableElements,
|
||||
Set<DriftElement> availableByDefault,
|
||||
List<FileState> imports
|
||||
})> _resolveElementsAndImports(BaseDriftAccessor element) async {
|
||||
final imports = <FileState>[];
|
||||
|
||||
for (final include in element.declaredIncludes) {
|
||||
final imported = await driver.resolveElements(driver.backend
|
||||
.resolveUri(element.declaration.sourceUri, include.toString()));
|
||||
|
||||
imports.add(imported);
|
||||
}
|
||||
|
||||
final imported = driver.cache.crawlMulti(imports).toSet();
|
||||
for (final import in imported) {
|
||||
await driver.resolveElements(import.ownUri);
|
||||
}
|
||||
|
||||
final availableByDefault = <DriftSchemaElement>{
|
||||
...element.declaredTables,
|
||||
...element.declaredViews,
|
||||
};
|
||||
|
||||
// For indices added to tables via an annotation, the index should
|
||||
// also be available.
|
||||
for (final table in element.declaredTables) {
|
||||
final fileState = driver.cache.knownFiles[table.id.libraryUri]!;
|
||||
|
||||
for (final attachedIndex in table.attachedIndices) {
|
||||
final index =
|
||||
await driver.resolveElement(fileState, fileState.id(attachedIndex));
|
||||
|
||||
if (index is DriftIndex) {
|
||||
availableByDefault.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final availableElements = imported
|
||||
.expand((reachable) {
|
||||
final elementAnalysis = reachable.analysis.values;
|
||||
|
||||
return elementAnalysis
|
||||
.map((e) => e.result)
|
||||
.where((e) => e is DefinedSqlQuery || e is DriftSchemaElement);
|
||||
})
|
||||
.whereType<DriftElement>()
|
||||
.where((e) {
|
||||
// Exclude any private tables that do not reside in the same library
|
||||
// as the DriftDatabase.
|
||||
// Failure to exclude these, can generate dart code which references
|
||||
// classes that cannot be legally accessed - and will not compile.
|
||||
// Private classes residing in the same library are allowed, as
|
||||
// per dart language accessibility rules.
|
||||
if (e is DriftElementWithResultSet &&
|
||||
e.entityInfoName.startsWith(r'$_')) {
|
||||
return e.id.libraryUri == element.id.libraryUri;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.followedBy(availableByDefault)
|
||||
.transitiveClosureUnderReferences()
|
||||
.sortTopologicallyOrElse(driver.backend.log.severe);
|
||||
|
||||
return (
|
||||
availableElements: availableElements,
|
||||
availableByDefault: availableByDefault,
|
||||
imports: imports
|
||||
);
|
||||
}
|
||||
|
||||
_OptionsAndRequiredVariables _createOptionsAndVars(
|
||||
SqlEngine engine,
|
||||
DeclaredStatement stmt,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: drift_dev
|
||||
description: Dev-dependency for users of drift. Contains the generator and development tools.
|
||||
version: 2.28.1
|
||||
version: 2.28.2
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
@@ -37,7 +37,7 @@ dependencies:
|
||||
# Drift-specific analysis and apis
|
||||
drift: ">=2.28.0 <2.29.0"
|
||||
sqlite3: ^2.4.6
|
||||
sqlparser: ^0.41.1
|
||||
sqlparser: ^0.41.2
|
||||
|
||||
# Dart analysis
|
||||
analyzer: '>=7.3.0 <9.0.0'
|
||||
@@ -46,11 +46,11 @@ dependencies:
|
||||
pub_semver: ^2.1.4
|
||||
|
||||
# Build system
|
||||
build: ^3.0.0
|
||||
build: '>=3.0.0 <5.0.0'
|
||||
build_resolvers: ^3.0.0
|
||||
build_config: ^1.1.0
|
||||
dart_style: ">=2.3.7 <4.0.0"
|
||||
source_gen: ^3.0.0
|
||||
source_gen: '>=3.0.0 <5.0.0'
|
||||
string_scanner: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@@ -8,13 +8,10 @@ const _content = {
|
||||
'a|lib/main.drift': '''
|
||||
CREATE TABLE foo (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
content TEXT NOT NULL UNIQUE,
|
||||
content2 TEXT NOT NULL UNIQUE
|
||||
content TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
query: INSERT INTO foo VALUES (?, ?, ?)
|
||||
ON CONFLICT (content) DO NOTHING
|
||||
ON CONFLICT (content2) DO UPDATE SET content2 = 'duplicate';
|
||||
query: SELECT group_concat(content ORDER BY id) FROM foo;
|
||||
''',
|
||||
};
|
||||
|
||||
@@ -28,7 +25,7 @@ void main() {
|
||||
[
|
||||
isDriftError(
|
||||
allOf(
|
||||
contains('require sqlite version 3.35 or later'),
|
||||
contains('require sqlite 3.44 or later'),
|
||||
contains(
|
||||
'You can change the assumed sqlite version with build options.'),
|
||||
),
|
||||
@@ -42,7 +39,7 @@ void main() {
|
||||
_content,
|
||||
options: const DriftOptions.defaults(
|
||||
sqliteAnalysisOptions: SqliteAnalysisOptions(
|
||||
version: SqliteVersion.v3_35,
|
||||
version: SqliteVersion.v3_50,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -316,6 +316,23 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
|
||||
[DriftSqlType.int, DriftSqlType.string]);
|
||||
});
|
||||
|
||||
test('does not warn about type of raise expressions', () async {
|
||||
// Regression test for https://github.com/simolus3/drift/issues/3643
|
||||
final test = await TestBackend.inTest({
|
||||
'a|lib/a.drift': '''
|
||||
CREATE TABLE foo (
|
||||
bar TEXT
|
||||
);
|
||||
|
||||
CREATE TRIGGER test_trigger BEFORE UPDATE OF bar ON foo BEGIN
|
||||
SELECT RAISE(ABORT, 'nope');
|
||||
END;
|
||||
'''
|
||||
});
|
||||
await test.analyze('package:a/a.drift');
|
||||
test.expectNoErrors();
|
||||
});
|
||||
|
||||
test('can cast to DATETIME and BOOLEAN', () async {
|
||||
final backend = await TestBackend.inTest({
|
||||
'a|lib/a.drift': '''
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import 'package:build/build.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:drift_dev/src/backends/build/analyzer.dart';
|
||||
import 'package:drift_dev/src/backends/build/drift_builder.dart';
|
||||
import 'package:drift_dev/src/backends/build/exception.dart';
|
||||
import 'package:drift_dev/src/backends/build/preprocess_builder.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@@ -189,11 +185,8 @@ class Users extends Table {
|
||||
expect(
|
||||
logger.onRecord,
|
||||
emits(
|
||||
isA<LogRecord>()
|
||||
.having((e) => e.message, 'message',
|
||||
contains('Could not resolve Dart library package:a/main.dart'))
|
||||
.having(
|
||||
(e) => e.error, 'error', isA<SyntaxErrorInAssetException>()),
|
||||
isA<LogRecord>().having((e) => e.message, 'message',
|
||||
contains('Could not resolve Dart library package:a/main.dart')),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -503,8 +496,10 @@ class MyTable extends Table {
|
||||
}
|
||||
''',
|
||||
},
|
||||
logger: loggerThat(emits(emits(isA<LogRecord>().having((e) => e.message,
|
||||
'message', contains('will be included in this database: MyTable'))))),
|
||||
logger: loggerThat(emits(emitsThrough(isA<LogRecord>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains('will be included in this database: MyTable'))))),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -569,118 +564,6 @@ class AppDatabase extends $AppDatabase {
|
||||
}, outputs.dartOutputs, outputs.writer);
|
||||
});
|
||||
|
||||
test('does not read unecessary files', () async {
|
||||
final inputs = <String, String>{
|
||||
'a|lib/groups.drift': '''
|
||||
CREATE TABLE "groups" (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
''',
|
||||
'a|lib/members.drift': '''
|
||||
import 'groups.drift';
|
||||
import 'database.dart';
|
||||
|
||||
CREATE TABLE memberships (
|
||||
"group" INTEGER NOT NULL REFERENCES "groups"(id),
|
||||
"user" INTEGER NOT NULL REFERENCES "users" (id),
|
||||
PRIMARY KEY ("group", user)
|
||||
);
|
||||
''',
|
||||
'a|lib/database.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
}
|
||||
|
||||
@DriftDatabase(include: {'groups.drift', 'members.drift'})
|
||||
class MyDatabase {
|
||||
|
||||
}
|
||||
''',
|
||||
};
|
||||
final outputs = await emulateDriftBuild(inputs: inputs);
|
||||
final readAssets = outputs.readAssetsByBuilder;
|
||||
// Allow reading SDK or other package assets to set up the analyzer.
|
||||
final isFromExternalPackage =
|
||||
isA<AssetId>().having((e) => e.package, 'package', isNot('a'));
|
||||
|
||||
Matcher onlyReadsJsonsAnd(dynamic other) {
|
||||
return everyElement(
|
||||
anyOf(
|
||||
isA<AssetId>().having((e) => e.extension, 'extension', '.json'),
|
||||
isFromExternalPackage,
|
||||
other,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void expectReadsForBuilder(String input, Type builder, dynamic expected) {
|
||||
final actuallyRead = readAssets.remove((builder, input));
|
||||
expect(actuallyRead, expected);
|
||||
}
|
||||
|
||||
// 1. Preprocess builders read only the drift file itself and no other
|
||||
// files.
|
||||
for (final input in inputs.keys) {
|
||||
if (input.endsWith('.drift')) {
|
||||
expectReadsForBuilder(input, PreprocessBuilder, [makeAssetId(input)]);
|
||||
}
|
||||
}
|
||||
|
||||
// The discover builder needs to analyze Dart files, which in the current
|
||||
// resolver implementation means reading all transitive imports as well.
|
||||
// However, the discover builder should not read other drift files.
|
||||
for (final input in inputs.keys) {
|
||||
if (input.endsWith('.drift')) {
|
||||
expectReadsForBuilder(input, DriftDiscover,
|
||||
everyElement(anyOf(makeAssetId(input), isFromExternalPackage)));
|
||||
} else {
|
||||
expectReadsForBuilder(
|
||||
input,
|
||||
DriftDiscover,
|
||||
isNot(
|
||||
contains(
|
||||
isA<AssetId>().having((e) => e.extension, 'extension', '.drift'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Groups has no imports, so the analyzer shouldn't read any source files
|
||||
// apart from groups.
|
||||
expectReadsForBuilder('a|lib/groups.drift', DriftAnalyzer,
|
||||
onlyReadsJsonsAnd(makeAssetId('a|lib/groups.drift')));
|
||||
|
||||
// Members is analyzed next. We don't have analysis results for the dart
|
||||
// file yet, so unfortunately that will have to be analyzed twice. But we
|
||||
// shouldn't read groups again.
|
||||
expectReadsForBuilder('a|lib/members.drift', DriftAnalyzer,
|
||||
isNot(contains(makeAssetId('a|lib/groups.drift'))));
|
||||
|
||||
// Similarly, analyzing the Dart file should not read the includes since
|
||||
// those have already been analyzed.
|
||||
expectReadsForBuilder(
|
||||
'a|lib/database.dart',
|
||||
DriftAnalyzer,
|
||||
isNot(
|
||||
contains(
|
||||
isA<AssetId>().having((e) => e.extension, 'extension', '.drift'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// The final builder needs to run file analysis which requires resolving
|
||||
// the input file fully. Unfortunately, resolving queries also needs access
|
||||
// to the original source so there's not really anything we could test.
|
||||
expectReadsForBuilder('a|lib/database.dart', DriftBuilder, anything);
|
||||
|
||||
// Make sure we didn't forget an assertion.
|
||||
expect(readAssets, isEmpty);
|
||||
});
|
||||
|
||||
test('generates views from drift tables', () async {
|
||||
final debugLogger = Logger('driftBuild');
|
||||
debugLogger.onRecord.listen((e) => print(e.message));
|
||||
@@ -1161,7 +1044,7 @@ class MyDatabase extends \$MyDatabase {}
|
||||
);
|
||||
|
||||
if (fatalWarnings) {
|
||||
await expectLater(build, throwsA(isA<FatalWarningException>()));
|
||||
await expectLater(build, throwsA(anything));
|
||||
} else {
|
||||
await build;
|
||||
}
|
||||
@@ -1225,7 +1108,7 @@ CREATE TABLE b (foo INTEGER);
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}, skip: 'Detailed logs not available through testBuilders');
|
||||
|
||||
test('generates generic type converters correctly', () async {
|
||||
// Regression test for https://github.com/simolus3/drift/issues/3300
|
||||
@@ -1339,4 +1222,51 @@ class Groups extends Table {
|
||||
'a|lib/groups.drift.dart': anything,
|
||||
}, build.dartOutputs, build.writer);
|
||||
});
|
||||
|
||||
test(
|
||||
'warns when dao references table not added to main database',
|
||||
() async {
|
||||
await emulateDriftBuild(
|
||||
inputs: {
|
||||
'a|lib/database.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'dao.dart';
|
||||
|
||||
class Users extends Table {
|
||||
late final id = integer().autoIncrement()();
|
||||
late final name = text()();
|
||||
}
|
||||
|
||||
@DriftDatabase(daos: [TestAccessor])
|
||||
class AppDatabase {}
|
||||
''',
|
||||
'a|lib/dao.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'database.dart';
|
||||
|
||||
@DriftAccessor(tables: [Users])
|
||||
class TestAccessor extends DatabaseAccessor<AppDatabase> {
|
||||
}
|
||||
''',
|
||||
},
|
||||
modularBuild: true,
|
||||
logger: loggerThat(
|
||||
emits(
|
||||
emits(
|
||||
isA<LogRecord>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains(
|
||||
"TestAccessor references tables that aren't available on the "
|
||||
'main database: [users]',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+38
-156
@@ -1,26 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:analyzer/dart/analysis/features.dart';
|
||||
import 'package:analyzer/dart/analysis/utilities.dart';
|
||||
import 'package:analyzer/file_system/memory_file_system.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:build/experiments.dart';
|
||||
import 'package:build/src/state/asset_finder.dart';
|
||||
import 'package:build/src/state/asset_path_provider.dart';
|
||||
import 'package:build/src/state/filesystem.dart';
|
||||
import 'package:build/src/state/filesystem_cache.dart';
|
||||
import 'package:build/src/state/generated_asset_hider.dart';
|
||||
import 'package:build/src/state/reader_writer.dart';
|
||||
import 'package:build_resolvers/build_resolvers.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:build_test/src/in_memory_reader_writer.dart';
|
||||
import 'package:build/src/state/reader_state.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:drift_dev/integrations/build.dart';
|
||||
import 'package:glob/glob.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
@@ -44,18 +32,6 @@ TypeMatcher<LogRecord> record(dynamic message) {
|
||||
return isA<LogRecord>().having((e) => e.message, 'message', message);
|
||||
}
|
||||
|
||||
final _packageConfig = Future(() async {
|
||||
final uri = await Isolate.packageConfig;
|
||||
|
||||
if (uri == null) {
|
||||
throw UnsupportedError(
|
||||
'Isolate running the build does not have a package config and no '
|
||||
'fallback has been provided');
|
||||
}
|
||||
|
||||
return await loadPackageConfigUri(uri);
|
||||
});
|
||||
|
||||
Future<TestReaderWriter> driftTestEnvironment(
|
||||
{String rootPackage = 'a'}) async {
|
||||
final rw = TestReaderWriter(rootPackage: rootPackage);
|
||||
@@ -89,88 +65,59 @@ Future<DriftBuildResult> emulateDriftBuild({
|
||||
}) async {
|
||||
_resolvers.reset();
|
||||
logger ??= Logger.detached('emulateDriftBuild');
|
||||
final logLines = <LogRecord>[];
|
||||
|
||||
final readAssets = <(Type, String), Set<AssetId>>{};
|
||||
final deletedAssets = <AssetId>[];
|
||||
final env = await driftTestEnvironment();
|
||||
inputs.forEach((id, contents) {
|
||||
env.testing.writeString(makeAssetId(id), contents);
|
||||
});
|
||||
|
||||
final stages = [
|
||||
preparingBuilder(options),
|
||||
discover(options),
|
||||
analyzer(options),
|
||||
modularBuild ? modular(options) : driftBuilderNotShared(options),
|
||||
driftCleanup(options),
|
||||
];
|
||||
|
||||
for (final stage in stages) {
|
||||
if (stage is Builder) {
|
||||
// We might want to consider running these concurrently, but tests are
|
||||
// easier to debug when running builders in a serial order.
|
||||
for (final input in inputs.keys) {
|
||||
final inputId = makeAssetId(input);
|
||||
|
||||
// Assets from other packages are visible, but we're not running
|
||||
// builders on them.
|
||||
if (inputId.package != 'a') continue;
|
||||
|
||||
if (expectedOutputs(stage, inputId).isNotEmpty) {
|
||||
final readerForPhase = _TrackingAssetReader(env);
|
||||
|
||||
await runBuilder(
|
||||
stage,
|
||||
[inputId],
|
||||
readerForPhase,
|
||||
env,
|
||||
_resolvers,
|
||||
logger: logger,
|
||||
packageConfig: await _packageConfig,
|
||||
);
|
||||
|
||||
readAssets.putIfAbsent(
|
||||
(stage.runtimeType, input), () => {}).addAll(readerForPhase.read);
|
||||
}
|
||||
final result = await testBuilders(
|
||||
[
|
||||
preparingBuilder(options),
|
||||
discover(options),
|
||||
analyzer(options),
|
||||
modularBuild ? modular(options) : driftBuilderNotShared(options),
|
||||
// TODO: Investigate testing post-process builder too. Once that's
|
||||
// possible, also patch DriftBuildResult.dartOutputs to use information
|
||||
// about deleted assets again.
|
||||
// driftCleanup(options),
|
||||
],
|
||||
inputs,
|
||||
rootPackage: 'a',
|
||||
onLog: (record) {
|
||||
logLines.add(record);
|
||||
if (record.level >= Level.WARNING) {
|
||||
// We sometimes want to assert that no warnings are printed, but
|
||||
// everything below that is noise.
|
||||
logger?.log(
|
||||
record.level, record.message, record.error, record.stackTrace);
|
||||
}
|
||||
} else if (stage is PostProcessBuilder) {
|
||||
for (final assetId in env.testing.assetsWritten) {
|
||||
final shouldBuild =
|
||||
stage.inputExtensions.any((e) => assetId.path.endsWith(e));
|
||||
if (shouldBuild) {
|
||||
await runPostProcessBuilder(
|
||||
stage,
|
||||
assetId,
|
||||
env,
|
||||
env,
|
||||
logger,
|
||||
addAsset: (_) {},
|
||||
deleteAsset: (id) {
|
||||
env.delete(id);
|
||||
deletedAssets.add(id);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
readerWriter: env,
|
||||
// Assets from other packages are visible, but we're not running
|
||||
// builders on them.
|
||||
generateFor:
|
||||
inputs.keys.where((e) => makeAssetId(e).package == 'a').toSet(),
|
||||
);
|
||||
if (result.buildResult.failureType != null) {
|
||||
throw Exception('testBuilders failed');
|
||||
}
|
||||
|
||||
final deletedAssets = <AssetId>[];
|
||||
|
||||
logger.clearListeners();
|
||||
return DriftBuildResult(env, readAssets, deletedAssets);
|
||||
return DriftBuildResult(env, deletedAssets);
|
||||
}
|
||||
|
||||
class DriftBuildResult {
|
||||
final TestReaderWriter writer;
|
||||
final List<AssetId> deleted;
|
||||
|
||||
/// Asset ids read for each (builder, input id) combination.
|
||||
final Map<(Type, String), Set<AssetId>> readAssetsByBuilder;
|
||||
|
||||
DriftBuildResult(this.writer, this.readAssetsByBuilder, this.deleted);
|
||||
DriftBuildResult(this.writer, this.deleted);
|
||||
|
||||
Iterable<AssetId> get dartOutputs {
|
||||
return writer.testing.assetsWritten.where((e) {
|
||||
return e.extension == '.dart' && !deleted.contains(e);
|
||||
return e.extension == '.dart' &&
|
||||
!deleted.contains(e) &&
|
||||
// TODO: Remove once we can test the post-process builder again.
|
||||
!e.path.endsWith('.temp.dart');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,71 +126,6 @@ class DriftBuildResult {
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackingAssetReader implements AssetReader, AssetReaderState {
|
||||
final TestReaderWriter _inner;
|
||||
|
||||
final Set<AssetId> read = {};
|
||||
|
||||
_TrackingAssetReader(this._inner);
|
||||
|
||||
void _trackRead(AssetId id) {
|
||||
read.add(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> canRead(AssetId id) {
|
||||
_trackRead(id);
|
||||
return _inner.canRead(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Digest> digest(AssetId id) {
|
||||
_trackRead(id);
|
||||
return _inner.digest(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<AssetId> findAssets(Glob glob) {
|
||||
return _inner.findAssets(glob).map((id) {
|
||||
_trackRead(id);
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> readAsBytes(AssetId id) {
|
||||
_trackRead(id);
|
||||
return _inner.readAsBytes(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) {
|
||||
_trackRead(id);
|
||||
return _inner.readAsString(id, encoding: encoding);
|
||||
}
|
||||
|
||||
@override
|
||||
AssetFinder get assetFinder => _inner.assetFinder;
|
||||
|
||||
@override
|
||||
AssetPathProvider get assetPathProvider => _inner.assetPathProvider;
|
||||
|
||||
@override
|
||||
FilesystemCache get cache => _inner.cache;
|
||||
|
||||
@override
|
||||
AssetReaderWriter copyWith(
|
||||
{FilesystemCache? cache, GeneratedAssetHider? generatedAssetHider}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Filesystem get filesystem => _inner.filesystem;
|
||||
|
||||
@override
|
||||
GeneratedAssetHider get generatedAssetHider => _inner.generatedAssetHider;
|
||||
}
|
||||
|
||||
extension ReaderWriterUtils on TestReaderWriter {
|
||||
String readGenerated(String assetId) {
|
||||
var id = AssetId.parse(assetId);
|
||||
|
||||
@@ -501,11 +501,11 @@ filterAlbums: SELECT
|
||||
final outputs = await emulateDriftBuild(
|
||||
inputs: {
|
||||
'a|lib/a.drift': '''
|
||||
import 'a.dart';
|
||||
import 'row.dart';
|
||||
|
||||
foo WITH MyRow.foo: SELECT 'hello world' AS a, 2 AS b;
|
||||
''',
|
||||
'a|lib/a.dart': '''
|
||||
'a|lib/row.dart': '''
|
||||
class MyRow {
|
||||
final String a;
|
||||
final int b;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## 0.2.6
|
||||
|
||||
- Add `initializeDatabase` parameter to `DriftWebOptions` to enable loading initial database.
|
||||
|
||||
## 0.2.5
|
||||
|
||||
- Fix `DriftWebOptions.onResult` not being called.
|
||||
|
||||
@@ -6,6 +6,9 @@ export 'unsupported.dart'
|
||||
if (dart.library.ffi) 'native.dart';
|
||||
|
||||
export 'package:drift/src/web/wasm_setup/types.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
// ignore: implementation_imports
|
||||
import 'package:drift/src/web/wasm_setup/types.dart';
|
||||
import 'package:sqlite3/common.dart';
|
||||
@@ -33,12 +36,15 @@ final class DriftWebOptions {
|
||||
/// impacted the database implementation (e.g. OPFS, IndexedDB) chosen.
|
||||
final void Function(WasmDatabaseResult)? onResult;
|
||||
|
||||
final FutureOr<Uint8List?> Function()? initializeDatabase;
|
||||
|
||||
/// Create web-specific drift options.
|
||||
DriftWebOptions({
|
||||
required this.sqlite3Wasm,
|
||||
required this.driftWorker,
|
||||
this.onResult,
|
||||
});
|
||||
DriftWebOptions(
|
||||
{required this.sqlite3Wasm,
|
||||
required this.driftWorker,
|
||||
this.onResult,
|
||||
this.initializeDatabase,
|
||||
});
|
||||
}
|
||||
|
||||
/// Options used to open drift databases on native platforms (outside of the
|
||||
|
||||
@@ -18,6 +18,7 @@ DriftConnection driftDatabase({
|
||||
databaseName: name,
|
||||
sqlite3Uri: web.sqlite3Wasm,
|
||||
driftWorkerUri: web.driftWorker,
|
||||
initializeDatabase: web.initializeDatabase,
|
||||
);
|
||||
|
||||
(web.onResult ?? _defaultResultHandler)(result);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: drift_flutter
|
||||
description: Easily set up drift databases across platforms in Flutter apps.
|
||||
version: 0.2.5
|
||||
version: 0.2.6
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
||||
@@ -9,8 +9,9 @@ class MariaDBDatabase extends DelegatedDatabase {
|
||||
required MySQLConnectionPool pool,
|
||||
bool isSequential = true,
|
||||
bool logStatements = false,
|
||||
bool enableMigrations = true,
|
||||
}) : super(
|
||||
_MariaDelegate(() => pool, true),
|
||||
_MariaDelegate(() => pool, true, enableMigrations),
|
||||
isSequential: isSequential,
|
||||
logStatements: logStatements,
|
||||
);
|
||||
@@ -20,8 +21,9 @@ class MariaDBDatabase extends DelegatedDatabase {
|
||||
MariaDBDatabase.opened(
|
||||
MySQLConnectionPool connection, {
|
||||
bool logStatements = false,
|
||||
bool enableMigrations = true,
|
||||
}) : super(
|
||||
_MariaDelegate(() => connection, false),
|
||||
_MariaDelegate(() => connection, false, enableMigrations),
|
||||
isSequential: true,
|
||||
logStatements: logStatements,
|
||||
);
|
||||
@@ -31,9 +33,14 @@ class MariaDBDatabase extends DelegatedDatabase {
|
||||
}
|
||||
|
||||
class _MariaDelegate extends DatabaseDelegate {
|
||||
_MariaDelegate(this._open, this.closeUnderlyingWhenClosed);
|
||||
_MariaDelegate(
|
||||
this._open,
|
||||
this.closeUnderlyingWhenClosed,
|
||||
this.enableMigrations,
|
||||
);
|
||||
|
||||
final bool closeUnderlyingWhenClosed;
|
||||
final bool enableMigrations;
|
||||
final FutureOr<MySQLConnectionPool> Function() _open;
|
||||
|
||||
MySQLConnectionPool? _openedSession;
|
||||
@@ -57,12 +64,16 @@ class _MariaDelegate extends DatabaseDelegate {
|
||||
@override
|
||||
Future<void> open(QueryExecutorUser user) async {
|
||||
final session = await _open();
|
||||
final mariaVersionDelegate = _MariaVersionDelegate(session);
|
||||
|
||||
await mariaVersionDelegate.init();
|
||||
if (enableMigrations) {
|
||||
final mariaVersionDelegate = _MariaVersionDelegate(session);
|
||||
await mariaVersionDelegate.init();
|
||||
versionDelegate = mariaVersionDelegate;
|
||||
} else {
|
||||
versionDelegate = NoVersionDelegate();
|
||||
}
|
||||
|
||||
_openedSession = session;
|
||||
versionDelegate = mariaVersionDelegate;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+40
-40
@@ -85,10 +85,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "6439a9c71a4e6eca8d9490c1b380a25b02675aa688137dfbe66d2062884a23ac"
|
||||
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.1.0"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -109,42 +109,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_modules
|
||||
sha256: "78f00844db79736785b1f00aa416bf8187936a4033246104f299e60c8ba73531"
|
||||
sha256: "5d599f7213e465cbc5193f73d7b323a9e6d7d300734b63a63eb99500cba259ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.16"
|
||||
version: "5.0.17"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "2b21a125d66a86b9511cc3fb6c668c42e9a1185083922bf60e46d483a81a9712"
|
||||
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
build_runner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: fd3c09f4bbff7fa6e8d8ef688a0b2e8a6384e6483a25af0dac75fef362bcfe6f
|
||||
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
version: "2.7.1"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: ab27e46c8aa233e610cf6084ee6d8a22c6f873a0a9929241d8855b7a72978ae7
|
||||
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
version: "9.3.1"
|
||||
build_test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_test
|
||||
sha256: "995d0ef7273d8eebc1ce3c1ec1d3a958d5a059d0a020cc888c049d8e7f1a38ad"
|
||||
sha256: "054eef426951304e127e800e0f76c3c978e7522d990532949380ae5e7224e65f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
version: "3.3.3"
|
||||
build_verify:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_web_compilers
|
||||
sha256: "7a6bb81907c3cdcc538af003db497cb9af37c94991bc960f9f25affd7a15df15"
|
||||
sha256: f730cf4decb4e3906cd93a8777500aa8daf55dac64f46cf5ec33b83d2cf635c5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
version: "4.2.3"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -173,10 +173,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
|
||||
sha256: "1b3b173f3379c8f941446267868548b6fc67e9134d81f4842eb98bb729451359"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.11.1"
|
||||
version: "8.11.2"
|
||||
built_value_generator:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -261,10 +261,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_snippets
|
||||
sha256: "940285a81344dbf8490d91f4da9943e3f12e85755b2739220cc8899a4118bf92"
|
||||
sha256: "664d32edf8e430dfafcff91bfde262745e7337eb40bad4aec9356a52a939cc0d"
|
||||
url: "https://pub-simonbinder-eu.fsn1.your-objectstorage.com"
|
||||
source: hosted
|
||||
version: "0.0.20"
|
||||
version: "0.0.21"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -447,10 +447,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2
|
||||
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.1"
|
||||
version: "10.3.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -489,10 +489,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
|
||||
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.29"
|
||||
version: "2.0.30"
|
||||
flutter_riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -536,10 +536,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "8b1f37dfaf6e958c6b872322db06f946509433bec3de753c3491a42ae9ec2b48"
|
||||
sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.1.0"
|
||||
version: "16.2.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -637,10 +637,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: ce2cf974ccdee13be2a510832d7fba0b94b364e0b0395dee42abaa51b855be27
|
||||
sha256: "3f2913b7c2430afe8ac5afe6fb15c1de4a60af4f630625e6e238f80ba4b80cbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.10.0"
|
||||
version: "6.11.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -701,10 +701,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: melos
|
||||
sha256: cc4e1b4313e96579ab5031a66f2ac46d2e96709e4714496bbc3a2305789d7ecc
|
||||
sha256: "44b2cff0bf2f8bcd88750433220f1b7420403175e016b3fcb05b0359164f9725"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "7.1.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -797,10 +797,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
version: "2.2.18"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -941,10 +941,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1029,10 +1029,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scratch_space
|
||||
sha256: "816989dd0a1f92cd5f0db012ed330035571034956dc3593fba66aaa6ee6a7e43"
|
||||
sha256: efe807ddf27b148b500465957ceee4255deb4a8558575530b6398840436300ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1178,10 +1178,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_sqlcipher
|
||||
sha256: edf10c7dc18e6bc4e9213c265d26856d303f0b0537174151d28c936b80f80bd0
|
||||
sha256: "81f690c8c250ecbcd981d5c7b98abbc2767a161ed2cdd5277b505cd4d95c681e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.3.0"
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1362,10 +1362,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
|
||||
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.17"
|
||||
version: "6.3.18"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1442,10 +1442,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## 0.41.2
|
||||
|
||||
- Improve error message on unknown columns when it looks like the identifier
|
||||
should have been a string literal.
|
||||
- Infer the output of `lag`, `lead` and `nth_value` to be nullable.
|
||||
|
||||
## 0.41.1
|
||||
|
||||
- Support new features introduced in SQLite version 3.50.0.
|
||||
|
||||
@@ -121,7 +121,11 @@ class ReferenceResolver
|
||||
|
||||
void _reportUnknownColumnError(Reference e, {Iterable<Column>? columns}) {
|
||||
final msg = StringBuffer('Unknown column.');
|
||||
if (columns != null) {
|
||||
|
||||
if (e.isSingleDoubleQuotedToken) {
|
||||
msg.write(' Note: Double-quotes define an identifier in SQL. '
|
||||
'If you meant to write a string literal, use single quotes instead.');
|
||||
} else if (columns != null) {
|
||||
final columnNames =
|
||||
columns.map((c) => c.humanReadableDescription()).join(', ');
|
||||
msg.write(' These columns are available: $columnNames');
|
||||
|
||||
@@ -157,7 +157,7 @@ class ResolveResult {
|
||||
/// Whether type resolution failed.
|
||||
final bool unknown;
|
||||
|
||||
const ResolveResult(this.type)
|
||||
const ResolveResult(ResolvedType this.type)
|
||||
: needsContext = false,
|
||||
unknown = false;
|
||||
|
||||
|
||||
@@ -750,10 +750,12 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||
return const ResolveResult.needsContext();
|
||||
case 'first_value':
|
||||
case 'last_value':
|
||||
session._addRelation(CopyTypeFrom(e, params.first));
|
||||
return const ResolveResult.needsContext();
|
||||
case 'lag':
|
||||
case 'lead':
|
||||
case 'nth_value':
|
||||
session._addRelation(CopyTypeFrom(e, params.first));
|
||||
session._addRelation(CopyTypeFrom(e, params.first, makeNullable: true));
|
||||
return const ResolveResult.needsContext();
|
||||
case 'max':
|
||||
case 'min':
|
||||
@@ -856,6 +858,13 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||
visitExcept(e, e.where, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitRaiseExpression(RaiseExpression e, TypeExpectation arg) {
|
||||
// Don't set an inner basic type because there isn't one. This just
|
||||
// ensures the expression is marked as resolved.
|
||||
session._checkAndResolve(e, const ResolvedType(), arg);
|
||||
}
|
||||
|
||||
void _handleColumn(Column? column, [AstNode? context]) {
|
||||
if (column == null ||
|
||||
session.graph.knowsType(column) ||
|
||||
|
||||
@@ -28,6 +28,27 @@ class Reference extends Expression with ReferenceOwner {
|
||||
'When setting a schemaName, entityName must not be null either.',
|
||||
);
|
||||
|
||||
/// Returns whether this [Reference] is syntactically derived from a single
|
||||
/// identifier token wrapped in double quotes.
|
||||
///
|
||||
/// Because the strict "double quotes are identifiers and never string
|
||||
/// literals" rule enabled by drift and sqlparser can be surprising, we use
|
||||
/// this information to improve error messages and point this out
|
||||
/// specifically.
|
||||
bool get isSingleDoubleQuotedToken {
|
||||
if (schemaName != null || entityName != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first == last) {
|
||||
if (first case final IdentifierToken singleToken) {
|
||||
return singleToken.escaped;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||
return visitor.visitReference(this, arg);
|
||||
|
||||
@@ -120,8 +120,8 @@ final class _GeopolyFunctionHandler extends FunctionHandler {
|
||||
|
||||
if (argumentIndex < func.args.length) {
|
||||
return ResolveResult(func.args[argumentIndex]);
|
||||
} else if (func.otherArgs != null) {
|
||||
return ResolveResult(func.otherArgs);
|
||||
} else if (func.otherArgs case final otherArgsType?) {
|
||||
return ResolveResult(otherArgsType);
|
||||
} else {
|
||||
return ResolveResult.unknown();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: sqlparser
|
||||
description: Parses sqlite statements and performs static analysis on them
|
||||
version: 0.41.1
|
||||
version: 0.41.2
|
||||
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
|
||||
repository: https://github.com/simolus3/drift
|
||||
#homepage: https://drift.simonbinder.eu/
|
||||
|
||||
@@ -440,4 +440,14 @@ CREATE TABLE routes (
|
||||
expect(fromReferenced!.source.containingSet,
|
||||
result.rootScope.knownTables['points']);
|
||||
});
|
||||
|
||||
test('warns about identifiers that should have been strings', () {
|
||||
final result = SqlEngine().analyze('''SELECT 'hello ' || "world";''');
|
||||
|
||||
result.expectError(
|
||||
'"world"',
|
||||
type: AnalysisErrorType.referencedUnknownColumn,
|
||||
message: contains('Note: Double-quotes define an identifier in SQL.'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,4 +193,26 @@ WITH RECURSIVE
|
||||
expect(ctx.typeOf(column),
|
||||
ResolveResult(ResolvedType(nullable: true, type: BasicType.int)));
|
||||
});
|
||||
|
||||
test('lag is nullable', () {
|
||||
// https://github.com/simolus3/drift/issues/3635
|
||||
final engine = SqlEngine()
|
||||
..registerTableFromSql(
|
||||
'CREATE table tab (id INTEGER PRIMARY KEY, timestamp INTEGER);');
|
||||
|
||||
final ctx = engine.analyze('''
|
||||
WITH ranked_tabs AS (
|
||||
SELECT id, timestamp,
|
||||
LAG(id) OVER (ORDER BY timestamp) as prev_tab_id
|
||||
FROM tab
|
||||
)
|
||||
SELECT prev_tab_id
|
||||
FROM ranked_tabs
|
||||
WHERE id = :tab_id;
|
||||
''');
|
||||
|
||||
final root = ctx.root as SelectStatement;
|
||||
expect(root.resolvedColumns!.map(ctx.typeOf),
|
||||
[ResolveResult(ResolvedType(nullable: true, type: BasicType.int))]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ void main() {
|
||||
equals(const ResolvedType(type: BasicType.int)));
|
||||
|
||||
expect(resolver.session.typeOf(secondVar),
|
||||
equals(const ResolvedType(type: BasicType.text)));
|
||||
equals(const ResolvedType(type: BasicType.text, nullable: true)));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user