Merge branch 'stream-wait-for-close' into new-query-builder

This commit is contained in:
Simon Binder
2025-09-11 21:48:11 +02:00
34 changed files with 418 additions and 428 deletions
-1
View File
@@ -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"
+13 -5
View File
@@ -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
View File
@@ -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
+1
View File
@@ -274,6 +274,7 @@ void main() {
);
});
stream.cancel();
await db.close();
}
+15
View File
@@ -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(
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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
View File
@@ -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;
+4
View File
@@ -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.
+11 -5
View File
@@ -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
+1
View File
@@ -18,6 +18,7 @@ DriftConnection driftDatabase({
databaseName: name,
sqlite3Uri: web.sqlite3Wasm,
driftWorkerUri: web.driftWorker,
initializeDatabase: web.initializeDatabase,
);
(web.onResult ?? _defaultResultHandler)(result);
+1 -1
View File
@@ -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
View File
@@ -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:
+6
View File
@@ -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');
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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 -1
View File
@@ -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)));
});
});