mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
Add known tables option for external tables
For https://github.com/simolus3/drift/discussions/3681
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user