Add known tables option for external tables

For https://github.com/simolus3/drift/discussions/3681
This commit is contained in:
Simon Binder
2025-10-11 22:08:48 +02:00
parent 120dd1eca3
commit 76f1be803b
7 changed files with 144 additions and 12 deletions
+21
View File
@@ -228,6 +228,27 @@ Each type consists of an arbitrary word used to determine [column affinity](http
with drift also supporting `DATETIME` and `BOOLEAN` as type hints. Then, the
optional `NULL` keyword can be used to indicate whether the type is nullable.
### External tables
In addition to `known_functions`, you can also define external tables that are assumed
to be available to the database without being managed by drift.
```yaml
targets:
$default:
builders:
drift_dev:
options:
sql:
dialect: sqlite
options:
known_tables:
- "CREATE TABLE external_resource (id INTEGER PRIMARY KEY, another TEXT)"
```
Here, references to `external_resource` would not cause a warning. Drift would not generate
code for the `external_resource` table though.
## Recommended options
In general, we recommend using the default options.
+3 -1
View File
@@ -1,6 +1,8 @@
## 2.28.4-dev
## 2.29.0-dev
- Fix invalid schema code for views.
- Add the `known_tables` option. It allows defining external tables assumed to be available
without including them in the drift-managed schema.
## 2.28.3
+39 -1
View File
@@ -4,7 +4,15 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart'
show BasicType, ResolvedType, SchemaFromCreateTable, SqliteVersion;
show
BasicType,
EngineOptions,
ResolvedType,
SchemaFromCreateTable,
SqlEngine,
SqliteVersion,
Table,
TableInducingStatement;
import 'package:string_scanner/string_scanner.dart';
part '../generated/analysis/options.g.dart';
@@ -281,10 +289,14 @@ class SqliteAnalysisOptions {
final Map<String, KnownSqliteFunction> knownFunctions;
@_TableFromSql()
final List<Table> knownTables;
const SqliteAnalysisOptions({
this.modules = const [],
this.version,
this.knownFunctions = const {},
this.knownTables = const [],
});
factory SqliteAnalysisOptions.fromJson(Map json) {
@@ -294,6 +306,32 @@ class SqliteAnalysisOptions {
Map<String, Object?> toJson() => _$SqliteAnalysisOptionsToJson(this);
}
final class _TableFromSql extends JsonConverter<Table, String> {
const _TableFromSql();
@override
Table fromJson(String json) {
final engine = SqlEngine(EngineOptions(version: SqliteVersion.current));
final result = engine.parse(json);
if (result.errors.isNotEmpty) {
throw ArgumentError.value(json,
'Not a valid CREATE TABLE statement: ${result.errors.join('\n')}');
}
final root = result.rootNode;
if (root is! TableInducingStatement) {
throw ArgumentError.value(json, 'Not a valid CREATE TABLE statement');
}
return SchemaFromCreateTable().read(root);
}
@override
String toJson(Table object) {
throw UnsupportedError('Unused');
}
}
class KnownSqliteFunction {
final List<ResolvedType> argumentTypes;
final ResolvedType returnType;
@@ -10,10 +10,17 @@ class DriftOptionsExtension implements Extension {
@override
void register(SqlEngine engine) {
final knownFunctions = options.sqliteOptions?.knownFunctions;
final knownTables = options.sqliteOptions?.knownTables;
if (knownFunctions != null) {
engine.registerFunctionHandler(_CustomFunctions(knownFunctions));
}
if (knownTables != null) {
for (final knownTable in knownTables) {
engine.registerTable(knownTable);
}
}
}
}
@@ -182,7 +182,7 @@ class DriftResolver {
return resolveReferencedElement(owner, existing.ownId);
} else {
return InvalidReferenceResult(
InvalidReferenceError.noElementWichSuchName,
InvalidReferenceError.noElementWithSuchName,
'The referenced element, ${element.name3}, is not understood by drift.',
);
}
@@ -214,7 +214,7 @@ class DriftResolver {
if (candidates.isEmpty) {
return InvalidReferenceResult(
InvalidReferenceError.noElementWichSuchName,
InvalidReferenceError.noElementWithSuchName,
'`$reference` could not be found in any import.',
);
} else if (candidates.length > 1) {
@@ -248,6 +248,17 @@ abstract class LocalElementResolver<T extends DiscoveredElement> {
DriftAnalysisError Function(String msg) createError,
) async {
final result = await resolver.resolveReference(discovered.ownId, reference);
if (result
case InvalidReferenceResult(
error: InvalidReferenceError.noElementWithSuchName
)) {
final knownTables = resolver.driver.options.sqliteOptions?.knownTables;
if (knownTables != null && knownTables.any((e) => e.name == reference)) {
// This table is external, no need to emit a warning.
return null;
}
}
return handleReferenceResult(result, createError);
}
@@ -355,11 +366,11 @@ abstract class LocalElementResolver<T extends DiscoveredElement> {
Future<DriftElement> resolve();
}
abstract class ResolveReferencedElementResult {
sealed class ResolveReferencedElementResult {
const ResolveReferencedElementResult();
}
class ResolvedReferenceFound extends ResolveReferencedElementResult {
final class ResolvedReferenceFound extends ResolveReferencedElementResult {
final DriftElement element;
ResolvedReferenceFound(this.element);
@@ -370,24 +381,24 @@ enum InvalidReferenceError {
/// Reported by [DriftResolver.resolveReference] when no element with the
/// given name exists in transitive imports.
noElementWichSuchName,
noElementWithSuchName,
/// Reported by [DriftResolver.resolveReference] when more than one element
/// with the queried name was found.
ambigiousElements,
}
class InvalidReferenceResult extends ResolveReferencedElementResult {
final class InvalidReferenceResult extends ResolveReferencedElementResult {
final InvalidReferenceError error;
final String message;
InvalidReferenceResult(this.error, this.message);
}
class ReferencedElementCouldNotBeResolved
final class ReferencedElementCouldNotBeResolved
extends ResolveReferencedElementResult {}
class ReferencesItself extends ResolveReferencedElementResult {
final class ReferencesItself extends ResolveReferencedElementResult {
const ReferencesItself();
}
@@ -271,7 +271,12 @@ SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['modules', 'version', 'known_functions'],
allowedKeys: const [
'modules',
'version',
'known_functions',
'known_tables'
],
);
final val = SqliteAnalysisOptions(
modules: $checkedConvert(
@@ -293,10 +298,20 @@ SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
k as String, KnownSqliteFunction.fromJson(e as String)),
) ??
const {}),
knownTables: $checkedConvert(
'known_tables',
(v) =>
(v as List<dynamic>?)
?.map((e) => const _TableFromSql().fromJson(e as String))
.toList() ??
const []),
);
return val;
},
fieldKeyMap: const {'knownFunctions': 'known_functions'},
fieldKeyMap: const {
'knownFunctions': 'known_functions',
'knownTables': 'known_tables'
},
);
Map<String, dynamic> _$SqliteAnalysisOptionsToJson(
@@ -307,6 +322,8 @@ Map<String, dynamic> _$SqliteAnalysisOptionsToJson(
instance.version, const _SqliteVersionConverter().toJson),
'known_functions':
instance.knownFunctions.map((k, e) => MapEntry(k, e.toJson())),
'known_tables':
instance.knownTables.map(const _TableFromSql().toJson).toList(),
};
Value? _$JsonConverterFromJson<Json, Value>(
+36
View File
@@ -116,4 +116,40 @@ sqlite:
expect(() => KnownSqliteFunction.fromJson('int (boolean, )'), throws);
});
});
group('external tables', () {
final options = parse('''
sqlite:
known_tables:
- "CREATE TABLE external (foo TEXT, bar INTEGER) STRICT;"
''');
test('available in drift file', () async {
final test = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE internal (
foo TEXT REFERENCES external(foo)
);
''',
}, options: options);
await test.analyze('package:a/a.drift');
test.expectNoErrors();
});
test('available in dart file', () async {
final test = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
class Internal extends Table {
TextColumn get foo => text().nullable().customConstraint('REFERENCES external(foo)')();
}
''',
}, options: options);
await test.analyze('package:a/a.dart');
test.expectNoErrors();
});
});
}