Merge pull request #2946 from nikitadol/develop

Add support for geopoly Interface
This commit is contained in:
Simon Binder 2024-04-23 15:23:44 +02:00 committed by GitHub
commit 8865e9360f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 1217 additions and 326 deletions

View File

@ -180,7 +180,10 @@ We currently support the following extensions:
Functions like `highlight` or `bm25` are available as well.
- `rtree`: Static analysis support for the [R\*Tree](https://www.sqlite.org/rtree.html) extension.
Enabling this option is safe when using a `NativeDatabase` with `sqlite3_flutter_libs`,
which compiles sqlite3 with the R\*Tree extension enabled.
which compiles sqlite3 with the R*Tree extension enabled.
- [geopoly](https://www.sqlite.org/geopoly.html), a generalization of the R*Tree module supporting more complex
polygons. Note that this is not the case for most sqlite3 builds,
including the ones shipping with `sqlite3_flutter_libs`.
- `moor_ffi`: Enables support for functions that are only available when using a `NativeDatabase`. This contains `pow`, `sqrt` and a variety
of trigonometric functions. Details on those functions are available [here]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}).
- `math`: Assumes that sqlite3 was compiled with [math functions](https://www.sqlite.org/lang_mathfunc.html).

View File

@ -78,3 +78,35 @@ The `bm25`, `highlight` and `snippet` functions from fts5 can also be used in cu
It's not possible to declare fts5 tables, or queries on fts5 tables, in Dart.
You can learn more about the fts5 extension on [sqlite.org](https://www.sqlite.org/fts5.html).
## geopoly
The Geopoly module is an alternative interface to the [R-Tree](https://www.sqlite.org/rtree.html) extension
that uses the [GeoJSON](https://geojson.org/) notation ([RFC-7946](https://datatracker.ietf.org/doc/html/rfc7946))
to describe two-dimensional polygons.
Geopoly includes functions for detecting when one polygon is contained within or overlaps with another,
for computing the area enclosed by a polygon,
for doing linear transformations of polygons,
for rendering polygons as [SVG](https://en.wikipedia.org/wiki/SVG),
and other similar operations.
To enable the `geopoly` extension in drift files and compiled queries, modify the
[build options]({{ "../Generation options/index.md" | pageUrl }}) to include
`geopoly` in the `sqlite_module` section.
An example of creating a virtual table using this extension:
```sql
create virtual table geo using geopoly(geoID, a, b);
```
Sqlite will accept any types in additional columns (`geoID`, `a`, `b` from the example above),
so `drift` will generate a `DriftAny` type for these columns, which is not always convenient.
To avoid this, you can add types as in this example:
```sql
create virtual table geo using geopoly (
geoID INTEGER not null,
a INTEGER,
b
);
```
This will add hints to column types and then the Dart code will be more convenient to use
You can learn more about the geopoly extension on [sqlite.org](https://www.sqlite.org/geopoly.html).

View File

@ -29,6 +29,7 @@ targets:
modules:
- json1
- fts5
- geopoly
build_web_compilers:entrypoint:
generate_for:
- "web/drift_worker.dart"
@ -59,3 +60,4 @@ targets:
modules:
- json1
- fts5
- geopoly

View File

@ -0,0 +1,79 @@
/// https://www.sqlite.org/geopoly.html
/// The Geopoly Interface To The SQLite R*Tree Module
library geopoly;
import 'dart:typed_data';
import '../src/runtime/query_builder/query_builder.dart';
import '../src/runtime/types/mapping.dart';
/// The type used for the `_shape` column in virtual `GEOPOLY` tables.
///
/// This type is responsible for representing shape values in Dart. It is
/// created by drift when the `geopoly` extension is enabled and a `CREATE
/// VIRTUAL TABLE USING geopoly` table is declared in a `.drift` file.
final class GeopolyPolygonType implements CustomSqlType<GeopolyPolygon> {
/// Default constant constructor for the geopoly type.
const GeopolyPolygonType();
@override
String mapToSqlLiteral(GeopolyPolygon dartValue) {
throw UnimplementedError();
}
@override
Object mapToSqlParameter(GeopolyPolygon dartValue) {
switch (dartValue) {
case GeopolyPolygonString(:final value):
return value;
case GeopolyPolygonBlob(:final value):
return value;
}
}
@override
GeopolyPolygon read(Object fromSql) {
return switch (fromSql) {
Uint8List() => GeopolyPolygon.blob(fromSql),
String() => GeopolyPolygon.text(fromSql),
_ => throw UnimplementedError(),
};
}
@override
String sqlTypeName(GenerationContext context) {
throw UnimplementedError();
}
}
/// In Geopoly, a polygon can be text or a blob.
sealed class GeopolyPolygon {
const GeopolyPolygon._();
/// Creates a geopoly shape from a textual representation listing its points.
///
/// For details on the syntax for [value], see https://www.sqlite.org/geopoly.html.
const factory GeopolyPolygon.text(String value) = GeopolyPolygonString;
/// Creates a geopoly shape from the binary representation used by sqlite3.
const factory GeopolyPolygon.blob(Uint8List value) = GeopolyPolygonBlob;
}
/// A [GeopolyPolygon] being described as text.
final class GeopolyPolygonString extends GeopolyPolygon {
/// The textual description of the polygon.
final String value;
/// Creates a polygon from the underlying textual [value].
const GeopolyPolygonString(this.value) : super._();
}
/// A [GeopolyPolygon] being described as binary data.
final class GeopolyPolygonBlob extends GeopolyPolygon {
/// The binary description of the polygon.
final Uint8List value;
/// Creates a polygon from the underlying binary [value].
const GeopolyPolygonBlob(this.value) : super._();
}

View File

@ -7,3 +7,5 @@ export 'runtime/query_builder/query_builder.dart' show TableInfo;
export 'dsl/dsl.dart'
show Table, TableIndex, View, DriftDatabase, DriftAccessor;
export '../extensions/geopoly.dart';

View File

@ -1,5 +1,5 @@
import 'dart:core';
import 'dart:core' as core;
import 'dart:core';
import 'dart:typed_data';
import 'package:collection/collection.dart';

View File

@ -0,0 +1,3 @@
CREATE VIRTUAL TABLE geopoly_test USING geopoly(a);
area: SELECT geopoly_area(_shape) FROM geopoly_test WHERE rowid = ?;

View File

@ -0,0 +1,49 @@
@TestOn('vm')
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:drift/extensions/geopoly.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.dart';
import '../test_utils/database_vm.dart';
part 'geopoly_integration_test.g.dart';
void main() {
preferLocalSqlite3();
test(
'can access geopoly types',
() async {
final database = _GeopolyTestDatabase(NativeDatabase.memory());
expect(database.geopolyTest.shape.type, isA<GeopolyPolygonType>());
final id =
await database.geopolyTest.insertOne(GeopolyTestCompanion.insert(
shape: Value(GeopolyPolygon.text('[[0,0],[1,0],[0.5,1],[0,0]]')),
));
final area = await database.area(id).getSingle();
expect(area, 0.5);
},
skip: _canUseGeopoly()
? null
: 'Cannot test, your sqlite3 does not support geopoly.',
);
}
bool _canUseGeopoly() {
final db = sqlite3.openInMemory();
final result = db
.select('SELECT sqlite_compileoption_used(?)', ['ENABLE_GEOPOLY']).single;
db.dispose();
return result.values[0] == 1;
}
@DriftDatabase(include: {'geopoly.drift'})
class _GeopolyTestDatabase extends _$_GeopolyTestDatabase {
_GeopolyTestDatabase(super.e);
@override
int get schemaVersion => 1;
}

View File

@ -0,0 +1,226 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'geopoly_integration_test.dart';
// ignore_for_file: type=lint
class GeopolyTest extends Table
with
TableInfo<GeopolyTest, GeopolyTestData>,
VirtualTableInfo<GeopolyTest, GeopolyTestData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
GeopolyTest(this.attachedDatabase, [this._alias]);
static const VerificationMeta _shapeMeta = const VerificationMeta('shape');
late final GeneratedColumn<GeopolyPolygon> shape =
GeneratedColumn<GeopolyPolygon>('_shape', aliasedName, true,
type: const GeopolyPolygonType(),
requiredDuringInsert: false,
$customConstraints: '');
static const VerificationMeta _aMeta = const VerificationMeta('a');
late final GeneratedColumn<DriftAny> a = GeneratedColumn<DriftAny>(
'a', aliasedName, true,
type: DriftSqlType.any,
requiredDuringInsert: false,
$customConstraints: '');
@override
List<GeneratedColumn> get $columns => [shape, a];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'geopoly_test';
@override
VerificationContext validateIntegrity(Insertable<GeopolyTestData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('_shape')) {
context.handle(
_shapeMeta, shape.isAcceptableOrUnknown(data['_shape']!, _shapeMeta));
}
if (data.containsKey('a')) {
context.handle(_aMeta, a.isAcceptableOrUnknown(data['a']!, _aMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
GeopolyTestData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return GeopolyTestData(
shape: attachedDatabase.typeMapping
.read(const GeopolyPolygonType(), data['${effectivePrefix}_shape']),
a: attachedDatabase.typeMapping
.read(DriftSqlType.any, data['${effectivePrefix}a']),
);
}
@override
GeopolyTest createAlias(String alias) {
return GeopolyTest(attachedDatabase, alias);
}
@override
bool get dontWriteConstraints => true;
@override
String get moduleAndArgs => 'geopoly(a)';
}
class GeopolyTestData extends DataClass implements Insertable<GeopolyTestData> {
final GeopolyPolygon? shape;
final DriftAny? a;
const GeopolyTestData({this.shape, this.a});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (!nullToAbsent || shape != null) {
map['_shape'] =
Variable<GeopolyPolygon>(shape, const GeopolyPolygonType());
}
if (!nullToAbsent || a != null) {
map['a'] = Variable<DriftAny>(a);
}
return map;
}
GeopolyTestCompanion toCompanion(bool nullToAbsent) {
return GeopolyTestCompanion(
shape:
shape == null && nullToAbsent ? const Value.absent() : Value(shape),
a: a == null && nullToAbsent ? const Value.absent() : Value(a),
);
}
factory GeopolyTestData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return GeopolyTestData(
shape: serializer.fromJson<GeopolyPolygon?>(json['_shape']),
a: serializer.fromJson<DriftAny?>(json['a']),
);
}
factory GeopolyTestData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
GeopolyTestData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'_shape': serializer.toJson<GeopolyPolygon?>(shape),
'a': serializer.toJson<DriftAny?>(a),
};
}
GeopolyTestData copyWith(
{Value<GeopolyPolygon?> shape = const Value.absent(),
Value<DriftAny?> a = const Value.absent()}) =>
GeopolyTestData(
shape: shape.present ? shape.value : this.shape,
a: a.present ? a.value : this.a,
);
@override
String toString() {
return (StringBuffer('GeopolyTestData(')
..write('shape: $shape, ')
..write('a: $a')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(shape, a);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is GeopolyTestData &&
other.shape == this.shape &&
other.a == this.a);
}
class GeopolyTestCompanion extends UpdateCompanion<GeopolyTestData> {
final Value<GeopolyPolygon?> shape;
final Value<DriftAny?> a;
final Value<int> rowid;
const GeopolyTestCompanion({
this.shape = const Value.absent(),
this.a = const Value.absent(),
this.rowid = const Value.absent(),
});
GeopolyTestCompanion.insert({
this.shape = const Value.absent(),
this.a = const Value.absent(),
this.rowid = const Value.absent(),
});
static Insertable<GeopolyTestData> custom({
Expression<GeopolyPolygon>? shape,
Expression<DriftAny>? a,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (shape != null) '_shape': shape,
if (a != null) 'a': a,
if (rowid != null) 'rowid': rowid,
});
}
GeopolyTestCompanion copyWith(
{Value<GeopolyPolygon?>? shape, Value<DriftAny?>? a, Value<int>? rowid}) {
return GeopolyTestCompanion(
shape: shape ?? this.shape,
a: a ?? this.a,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (shape.present) {
map['_shape'] =
Variable<GeopolyPolygon>(shape.value, const GeopolyPolygonType());
}
if (a.present) {
map['a'] = Variable<DriftAny>(a.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('GeopolyTestCompanion(')
..write('shape: $shape, ')
..write('a: $a, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$_GeopolyTestDatabase extends GeneratedDatabase {
_$_GeopolyTestDatabase(QueryExecutor e) : super(e);
late final GeopolyTest geopolyTest = GeopolyTest(this);
Selectable<double?> area(int var1) {
return customSelect(
'SELECT geopoly_area(_shape) AS _c0 FROM geopoly_test WHERE "rowid" = ?1',
variables: [
Variable<int>(var1)
],
readsFrom: {
geopolyTest,
}).map((QueryRow row) => row.readNullable<double>('_c0'));
}
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [geopolyTest];
}

View File

@ -19,6 +19,8 @@ abstract class DriftBackend {
return element.source!.uri;
}
bool get canReadDart;
/// Resolves a Dart library by its uri.
///
/// This should also be able to resolve SDK libraries.

View File

@ -1,11 +1,12 @@
import 'dart:convert';
import 'package:analyzer/dart/element/element.dart';
import 'package:meta/meta.dart';
import 'package:sqlparser/sqlparser.dart';
import '../options.dart';
import '../backend.dart';
import '../drift_native_functions.dart';
import '../options.dart';
import '../resolver/dart/helper.dart';
import '../resolver/discover.dart';
import '../resolver/drift/sqlparser/mapping.dart';
@ -62,10 +63,32 @@ class DriftAnalysisDriver {
AnalysisResultCacheReader? cacheReader;
KnownDriftTypes? _knownTypes;
final KnownDriftTypes? _knownTypes;
DriftAnalysisDriver(this.backend, this.options, {bool isTesting = false})
: _isTesting = isTesting;
KnownDriftTypes get knownTypes => _knownTypes!;
@visibleForTesting
DriftAnalysisDriver(
this.backend,
this.options,
this._knownTypes, {
bool isTesting = false,
}) : _isTesting = isTesting;
static Future<DriftAnalysisDriver> init(
DriftBackend backend,
DriftOptions options, {
bool isTesting = false,
}) async {
final driver = DriftAnalysisDriver(
backend,
options,
await KnownDriftTypes.resolve(backend),
isTesting: isTesting,
);
return driver;
}
SqlEngine newSqlEngine() {
return SqlEngine(
@ -83,17 +106,13 @@ class DriftAnalysisDriver {
if (options.hasModule(SqlModule.rtree)) const RTreeExtension(),
if (options.hasModule(SqlModule.spellfix1))
const Spellfix1Extension(),
if (options.hasModule(SqlModule.geopoly)) const GeopolyExtension(),
],
version: options.sqliteVersion,
),
);
}
/// Loads types important for Drift analysis.
Future<KnownDriftTypes> loadKnownTypes() async {
return _knownTypes ??= await KnownDriftTypes.resolve(this);
}
/// For a given file under [uri], attempts to restore serialized analysis
/// results that have been stored before.
///
@ -358,6 +377,7 @@ abstract class AnalysisResultCacheReader {
Future<CachedDiscoveryResults?> readDiscovery(Uri uri);
Future<LibraryElement?> readTypeHelperFor(Uri uri);
Future<String?> readElementCacheFor(Uri uri);
}

View File

@ -414,6 +414,19 @@ enum SqlModule {
rtree,
spellfix1,
/// The Geopoly module is an alternative interface to the R-Tree extension
/// that uses the GeoJSON notation (RFC-7946)
/// to describe two-dimensional polygons.
///
/// Geopoly includes functions for detecting
/// when one polygon is contained within or overlaps with another,
/// for computing the area enclosed by a polygon
/// for doing linear transformations of polygons,
/// for rendering polygons as SVG, and other similar operations.
///
/// See more: https://www.sqlite.org/geopoly.html
geopoly,
}
/// The possible values for the case of the table and column names.

View File

@ -344,7 +344,7 @@ class ColumnParser {
.apply(getter.name.lexeme);
ColumnType columnType;
final helper = await _resolver.resolver.driver.loadKnownTypes();
final helper = _resolver.resolver.driver.knownTypes;
if (foundStartMethod == _startCustom) {
final expression = remainingExpr.argumentList.arguments.single;

View File

@ -7,7 +7,7 @@ import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:collection/collection.dart';
import '../../driver/driver.dart';
import '../../backend.dart';
import '../../driver/error.dart';
import '../../results/results.dart';
import '../resolver.dart';
@ -32,6 +32,7 @@ class KnownDriftTypes {
final InterfaceElement jsonTypeConverter;
final InterfaceType driftAny;
final InterfaceType uint8List;
final InterfaceType geopolyPolygon;
KnownDriftTypes._(
this.helperLibrary,
@ -47,6 +48,7 @@ class KnownDriftTypes {
this.driftAccessor,
this.driftAny,
this.uint8List,
this.geopolyPolygon,
);
/// Constructs the set of known drift types from a helper library, which is
@ -73,6 +75,8 @@ class KnownDriftTypes {
.defaultInstantiation,
(exportNamespace.get('Uint8List') as InterfaceElement)
.defaultInstantiation,
(exportNamespace.get('GeopolyPolygon') as InterfaceElement)
.defaultInstantiation,
);
}
@ -98,10 +102,14 @@ class KnownDriftTypes {
return type?.asInstanceOf(converter);
}
static Future<KnownDriftTypes> resolve(DriftAnalysisDriver driver) async {
final library = await driver.backend.readDart(uri);
static Future<KnownDriftTypes?> resolve(DriftBackend backend) async {
if (backend.canReadDart) {
final library = await backend.readDart(uri);
return KnownDriftTypes._fromLibrary(library);
return KnownDriftTypes._fromLibrary(library);
}
return null;
}
static final Uri uri = Uri.parse('package:drift/src/drift_dev_helper.dart');
@ -256,7 +264,7 @@ class DataClassInformation {
useRowClass.getField('constructor')!.toStringValue()!;
final generateInsertable =
useRowClass.getField('generateInsertable')!.toBoolValue()!;
final helper = await resolver.resolver.driver.loadKnownTypes();
final helper = resolver.resolver.driver.knownTypes;
if (type is InterfaceType) {
final found = FoundDartClass(type.element, type.typeArguments);

View File

@ -53,7 +53,7 @@ class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
Future<TableReferenceInDartView?> _getStaticReference(
FieldElement field) async {
final type = field.type;
final knownTypes = await resolver.driver.loadKnownTypes();
final knownTypes = resolver.driver.knownTypes;
final typeSystem = field.library.typeSystem;
if (type is! InterfaceType ||

View File

@ -73,8 +73,7 @@ class DiscoverStep {
_file.discovery = NotADartLibrary();
break;
}
final finder =
_FindDartElements(this, library, await _driver.loadKnownTypes());
final finder = _FindDartElements(this, library, _driver.knownTypes);
await finder.find();
_file.errorsDuringDiscovery.addAll(finder.errors);

View File

@ -37,7 +37,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
return null;
}
final knownTypes = await resolver.driver.loadKnownTypes();
final knownTypes = resolver.driver.knownTypes;
return readCustomType(
knownTypes.helperLibrary,
expression,
@ -64,7 +64,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
return null;
}
final knownTypes = await resolver.driver.loadKnownTypes();
final knownTypes = resolver.driver.knownTypes;
return readTypeConverter(
knownTypes.helperLibrary,
expression,
@ -153,7 +153,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
innerType,
false,
this,
await resolver.driver.loadKnownTypes(),
resolver.driver.knownTypes,
);
}
}
@ -166,7 +166,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
));
return null;
} else {
final knownTypes = await resolver.driver.loadKnownTypes();
final knownTypes = resolver.driver.knownTypes;
return validateExistingClass(columns, foundDartClass,
source.constructorName ?? '', false, this, knownTypes);
}

View File

@ -76,9 +76,11 @@ class TypeMapping {
var type = _driftTypeToParser(column.sqlType.builtin)
.withNullable(column.nullable);
if (column.sqlType.isCustom) {
type = type.addHint(CustomTypeHint(column.sqlType.custom!));
}
type = switch (column.sqlType) {
ColumnDriftType() => type,
ColumnCustomType(:final custom) => type.addHint(CustomTypeHint(custom)),
};
if (column.typeConverter case AppliedTypeConverter c) {
type = type.addHint(TypeConverterHint(c));
}
@ -147,6 +149,20 @@ class TypeMapping {
return ColumnType.custom(customHint.type);
}
if (type.hint<IsGeopolyPolygon>() != null) {
final knownTypes = driver.knownTypes;
return ColumnType.custom(
CustomColumnType(
AnnotatedDartCode.importedSymbol(
Uri.parse('package:drift/extensions/geopoly.dart'),
'const GeopolyPolygonType()',
),
knownTypes.geopolyPolygon,
),
);
}
return ColumnType.drift(_toDefaultType(type));
}
}

View File

@ -77,7 +77,7 @@ class DriftTableResolver extends DriftElementResolver<DiscoveredDriftTable> {
type.builtin == DriftSqlType.int
? EnumType.intEnum
: EnumType.textEnum,
await resolver.driver.loadKnownTypes(),
resolver.driver.knownTypes,
);
}
}

View File

@ -27,7 +27,7 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
? null
: await createTypeResolver(
allReferences,
await resolver.driver.loadKnownTypes(),
resolver.driver.knownTypes,
);
final context = engine.analyzeNode(

View File

@ -19,7 +19,7 @@ class FileAnalyzer {
Future<FileAnalysisResult> runAnalysisOn(FileState state) async {
final result = FileAnalysisResult();
final knownTypes = await driver.loadKnownTypes();
final knownTypes = driver.knownTypes;
if (state.extension == '.dart') {
for (final elementAnalysis in state.analysis.values) {

View File

@ -488,7 +488,7 @@ DartType regularColumnType(
extension on TypeProvider {
DartType typeFor(ColumnType type, KnownDriftTypes knownTypes) {
if (type.custom case CustomColumnType custom) {
if (type case ColumnCustomType(:final custom)) {
return custom.dartType;
}

View File

@ -131,18 +131,19 @@ class AnnotatedDartCodeBuilder {
void addDriftType(HasType hasType) {
void addNonListType() {
final converter = hasType.typeConverter;
final customType = hasType.sqlType.custom;
if (converter != null) {
final nullable = converter.canBeSkippedForNulls && hasType.nullable;
addDartType(converter.dartType);
if (nullable) addText('?');
} else if (customType != null) {
addDartType(customType.dartType);
if (hasType.nullable) addText('?');
} else {
addTopLevel(dartTypeNames[hasType.sqlType.builtin]!);
switch (hasType.sqlType) {
case ColumnDriftType():
addTopLevel(dartTypeNames[hasType.sqlType.builtin]!);
case ColumnCustomType(:final custom):
addDartType(custom.dartType);
}
if (hasType.nullable) addText('?');
}
}

View File

@ -118,6 +118,7 @@ abstract class SqlQuery {
final String name;
AnalysisContext? get fromContext;
AstNode? get root;
/// Whether this query was declared in a `.drift` file.
@ -474,6 +475,7 @@ class InferredResultSet {
});
Iterable<ScalarResultColumn> get scalarColumns => columns.whereType();
Iterable<NestedResult> get nestedResults => columns.whereType();
/// Whether a new class needs to be written to store the result of this query.
@ -747,7 +749,12 @@ final class ScalarResultColumn extends ResultColumn
}
int get _columnTypeCompatibilityHash {
return Object.hash(sqlType.builtin, sqlType.custom?.dartType);
final custom = switch (sqlType) {
ColumnDriftType() => null,
ColumnCustomType(:final custom) => custom,
};
return Object.hash(sqlType.builtin, custom?.dartType);
}
@override
@ -758,12 +765,29 @@ final class ScalarResultColumn extends ResultColumn
@override
bool isCompatibleTo(ResultColumn other) {
return other is ScalarResultColumn &&
if (other is ScalarResultColumn &&
other.name == name &&
other.sqlType.builtin == sqlType.builtin &&
other.sqlType.custom?.dartType == sqlType.custom?.dartType &&
other.nullable == nullable &&
other.typeConverter == typeConverter;
other.typeConverter == typeConverter) {
// ok
} else {
return false;
}
switch ((sqlType, other.sqlType)) {
case (
ColumnCustomType(:final custom),
ColumnCustomType(custom: final otherCustom)
):
if (custom.dartType != otherCustom.dartType) {
return false;
}
case _:
break;
}
return true;
}
}

View File

@ -36,7 +36,7 @@ abstract class HasType {
/// appears where an array is expected or has a type converter applied to it.
/// [HasType] is the interface for sql-typed elements and is implemented by
/// columns.
class ColumnType {
sealed class ColumnType {
/// The builtin drift type used by this column.
///
/// Even though it's unused there, custom types also have this field set -
@ -44,14 +44,21 @@ class ColumnType {
/// all.
final DriftSqlType builtin;
/// Details about the custom type, if one is present.
final CustomColumnType? custom;
const ColumnType._(this.builtin);
bool get isCustom => custom != null;
const factory ColumnType.drift(DriftSqlType builtin) = ColumnDriftType;
const ColumnType.drift(this.builtin) : custom = null;
const factory ColumnType.custom(CustomColumnType custom) = ColumnCustomType;
}
ColumnType.custom(CustomColumnType this.custom) : builtin = DriftSqlType.any;
final class ColumnDriftType extends ColumnType {
const ColumnDriftType(super.builtin) : super._();
}
final class ColumnCustomType extends ColumnType {
final CustomColumnType custom;
const ColumnCustomType(this.custom) : super._(DriftSqlType.any);
}
extension OperationOnTypes on HasType {

View File

@ -200,16 +200,16 @@ class ElementSerializer {
}
Map<String, Object?> _serializeColumnType(ColumnType type) {
final custom = type.custom;
return {
if (custom != null)
'custom': {
'dart': _serializeType(custom.dartType),
'expression': custom.expression.toJson(),
}
else
'builtin': type.builtin.name,
return switch (type) {
ColumnDriftType() => {
'builtin': type.builtin.name,
},
ColumnCustomType(:final custom) => {
'custom': {
'dart': _serializeType(custom.dartType),
'expression': custom.expression.toJson(),
}
},
};
}

View File

@ -46,11 +46,11 @@ class AnalysisContextBackend extends DriftBackend {
AnalysisContextBackend(this.context, this.provider);
static PhysicalDriftDriver createDriver({
static Future<PhysicalDriftDriver> createDriver({
DriftOptions options = const DriftOptions.defaults(),
ResourceProvider? resourceProvider,
required String projectDirectory,
}) {
}) async {
final underlyingProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE;
final provider = OverlayResourceProvider(underlyingProvider);
@ -62,7 +62,7 @@ class AnalysisContextBackend extends DriftBackend {
final context = contextCollection.contextFor(projectDirectory);
final backend = AnalysisContextBackend(context, provider);
final driver = DriftAnalysisDriver(backend, options);
final driver = await DriftAnalysisDriver.init(backend, options);
return PhysicalDriftDriver(driver, backend);
}
@ -96,6 +96,9 @@ class AnalysisContextBackend extends DriftBackend {
return Future.value(resourceProvider.getFile(path).readAsStringSync());
}
@override
bool get canReadDart => true;
@override
Future<LibraryElement> readDart(Uri uri) async {
final result = await context.currentSession.getLibraryByUri(uri.toString());

View File

@ -31,7 +31,7 @@ class DriftDiscover extends Builder {
@override
Future<void> build(BuildStep buildStep) async {
final backend = DriftBuildBackend(buildStep);
final driver = DriftAnalysisDriver(backend, options);
final driver = await DriftAnalysisDriver.init(backend, options);
final prepared = await driver.findLocalElements(buildStep.inputId.uri);
final discovery = prepared.discovery;
@ -84,7 +84,7 @@ class DriftAnalyzer extends Builder {
@override
Future<void> build(BuildStep buildStep) async {
final backend = DriftBuildBackend(buildStep);
final driver = DriftAnalysisDriver(backend, options)
final driver = await DriftAnalysisDriver.init(backend, options)
..cacheReader =
BuildCacheReader(buildStep, findsLocalElementsReliably: true);

View File

@ -38,6 +38,9 @@ class DriftBuildBackend extends DriftBackend {
return id.uri;
}
@override
bool get canReadDart => true;
@override
Future<LibraryElement> readDart(Uri uri) async {
if (uri.scheme == 'dart') {

View File

@ -1,5 +1,6 @@
import 'package:build/build.dart';
import 'package:dart_style/dart_style.dart';
import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart';
import '../../analysis/custom_result_class.dart';
@ -102,7 +103,7 @@ class DriftBuilder extends Builder {
@override
Future<void> build(BuildStep buildStep) async {
final run = _DriftBuildRun(options, generationMode, buildStep);
final run = await _DriftBuildRun.init(options, generationMode, buildStep);
await run.run();
}
}
@ -133,18 +134,31 @@ class _DriftBuildRun {
Set<Uri> analyzedUris = {};
bool _didPrintWarning = false;
_DriftBuildRun(this.options, this.mode, this.buildStep)
: driver = DriftAnalysisDriver(DriftBuildBackend(buildStep), options)
..cacheReader = BuildCacheReader(
buildStep,
// The discovery and analyzer builders will have emitted IR for
// every relevant file in a previous build step that this builder
// has a dependency on.
findsResolvedElementsReliably:
!mode.embeddedAnalyzer || options.hasDriftAnalyzer,
findsLocalElementsReliably:
!mode.embeddedAnalyzer || options.hasDriftAnalyzer,
);
@visibleForTesting
_DriftBuildRun(this.options, this.mode, this.buildStep, this.driver);
static Future<_DriftBuildRun> init(
DriftOptions options,
DriftGenerationMode mode,
BuildStep buildStep,
) async {
return _DriftBuildRun(
options,
mode,
buildStep,
await DriftAnalysisDriver.init(DriftBuildBackend(buildStep), options)
..cacheReader = BuildCacheReader(
buildStep,
// The discovery and analyzer builders will have emitted IR for
// every relevant file in a previous build step that this builder
// has a dependency on.
findsResolvedElementsReliably:
!mode.embeddedAnalyzer || options.hasDriftAnalyzer,
findsLocalElementsReliably:
!mode.embeddedAnalyzer || options.hasDriftAnalyzer,
),
);
}
Future<void> run() async {
await _warnAboutDeprecatedOptions();

View File

@ -184,6 +184,7 @@ const _$SqlModuleEnumMap = {
SqlModule.math: 'math',
SqlModule.rtree: 'rtree',
SqlModule.spellfix1: 'spellfix1',
SqlModule.geopoly: 'geopoly',
};
const _$CaseFromDartToSqlEnumMap = {

View File

@ -21,7 +21,7 @@ Future<List<DriftElement>> extractDriftElementsFromDatabase(
final logger = Logger('extractDriftElementsFromDatabase');
final uri = Uri.parse('db.drift');
final backend = _SingleFileNoAnalyzerBackend(logger, uri);
final driver = DriftAnalysisDriver(
final driver = await DriftAnalysisDriver.init(
backend,
DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(
@ -91,6 +91,9 @@ class _SingleFileNoAnalyzerBackend extends DriftBackend {
return Future.value(contents);
}
@override
bool get canReadDart => false;
@override
Future<LibraryElement> readDart(Uri uri) async {
_noAnalyzer();

View File

@ -2,10 +2,10 @@ import 'package:drift/drift.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import '../../analysis/resolver/queries/nested_queries.dart';
import '../../analysis/results/results.dart';
import '../../analysis/options.dart';
import '../../analysis/resolver/queries/explicit_alias_transformer.dart';
import '../../analysis/resolver/queries/nested_queries.dart';
import '../../analysis/results/results.dart';
import '../../utils/string_escaper.dart';
import '../writer.dart';
import 'result_set_writer.dart';
@ -31,6 +31,7 @@ class QueryWriter {
late final ExplicitAliasTransformer _transformer;
final TextEmitter _emitter;
StringBuffer get _buffer => _emitter.buffer;
DriftOptions get options => scope.writer.options;
@ -207,13 +208,14 @@ class QueryWriter {
_emitter.dartCode(_emitter.innerColumnType(column.sqlType));
String code;
if (column.sqlType.isCustom) {
final method = isNullable ? 'readNullableWithType' : 'readWithType';
final typeImpl = _emitter.dartCode(column.sqlType.custom!.expression);
code = 'row.$method<$rawDartType>($typeImpl, $dartLiteral)';
} else {
final method = isNullable ? 'readNullable' : 'read';
code = 'row.$method<$rawDartType>($dartLiteral)';
switch (column.sqlType) {
case ColumnDriftType():
final method = isNullable ? 'readNullable' : 'read';
code = 'row.$method<$rawDartType>($dartLiteral)';
case ColumnCustomType(:final custom):
final method = isNullable ? 'readNullableWithType' : 'readWithType';
final typeImpl = _emitter.dartCode(custom.expression);
code = 'row.$method<$rawDartType>($typeImpl, $dartLiteral)';
}
final converter = column.typeConverter;
@ -759,6 +761,7 @@ class _ExpandedDeclarationWriter {
class _ExpandedVariableWriter {
final SqlQuery query;
final TextEmitter _emitter;
StringBuffer get _buffer => _emitter.buffer;
_ExpandedVariableWriter(this.query, this._emitter);

View File

@ -13,6 +13,7 @@ class DataClassWriter {
bool get isInsertable => table is DriftTable;
final TextEmitter _emitter;
StringBuffer get _buffer => _emitter.buffer;
DataClassWriter(this.table, this.scope) : _emitter = scope.leaf();
@ -341,11 +342,12 @@ class RowMappingWriter {
final columnName = column.nameInSql;
final rawData = "data['\${effectivePrefix}$columnName']";
String sqlType;
if (column.sqlType.custom case CustomColumnType custom) {
sqlType = writer.dartCode(custom.expression);
} else {
sqlType = writer.drift(column.sqlType.builtin.toString());
final String sqlType;
switch (column.sqlType) {
case ColumnDriftType():
sqlType = writer.drift(column.sqlType.builtin.toString());
case ColumnCustomType(:final custom):
sqlType = writer.dartCode(custom.expression);
}
var loadType = '$databaseGetter.typeMapping.read($sqlType, $rawData)';

View File

@ -11,6 +11,7 @@ import 'update_companion_writer.dart';
/// Both classes need to generate column getters and a mapping function.
abstract class TableOrViewWriter {
DriftElementWithResultSet get tableOrView;
TextEmitter get emitter;
StringBuffer get buffer => emitter.buffer;
@ -209,12 +210,12 @@ abstract class TableOrViewWriter {
}
}
if (column.sqlType.isCustom) {
additionalParams['type'] =
emitter.dartCode(column.sqlType.custom!.expression);
} else {
additionalParams['type'] =
emitter.drift(column.sqlType.builtin.toString());
switch (column.sqlType) {
case ColumnDriftType():
additionalParams['type'] =
emitter.drift(column.sqlType.builtin.toString());
case ColumnCustomType(:final custom):
additionalParams['type'] = emitter.dartCode(custom.expression);
}
if (isRequiredForInsert != null) {

View File

@ -26,6 +26,7 @@ class Writer extends _NodeOrWriter {
final GenerationOptions generationOptions;
TextEmitter get header => _header;
TextEmitter get imports => _imports;
@override
@ -51,6 +52,7 @@ class Writer extends _NodeOrWriter {
}
Scope child() => _root.child();
TextEmitter leaf() => _root.leaf();
}
@ -143,12 +145,12 @@ abstract class _NodeOrWriter {
return AnnotatedDartCode.build((b) {
AnnotatedDartCode sqlDartType;
if (converter.sqlType.isCustom) {
sqlDartType =
AnnotatedDartCode.type(converter.sqlType.custom!.dartType);
} else {
sqlDartType =
AnnotatedDartCode([dartTypeNames[converter.sqlType.builtin]!]);
switch (converter.sqlType) {
case ColumnDriftType():
sqlDartType =
AnnotatedDartCode([dartTypeNames[converter.sqlType.builtin]!]);
case ColumnCustomType(:final custom):
sqlDartType = AnnotatedDartCode.type(custom.dartType);
}
final className = converter.alsoAppliesToJsonConversion
@ -205,12 +207,11 @@ abstract class _NodeOrWriter {
/// This type does not respect type converters or arrays.
AnnotatedDartCode innerColumnType(ColumnType type, {bool nullable = false}) {
return AnnotatedDartCode.build((b) {
final custom = type.custom;
if (custom != null) {
b.addDartType(custom.dartType);
} else {
b.addTopLevel(dartTypeNames[type.builtin]!);
switch (type) {
case ColumnDriftType():
b.addTopLevel(dartTypeNames[type.builtin]!);
case ColumnCustomType(:final custom):
b.addDartType(custom.dartType);
}
if (nullable) {
@ -241,12 +242,15 @@ abstract class _NodeOrWriter {
b.addCode(expression);
}
if (column.sqlType.isCustom) {
// Also specify the custom type since it can't be inferred from the
// value passed to the variable.
b
..addText(', ')
..addCode(column.sqlType.custom!.expression);
switch (column.sqlType) {
case ColumnDriftType():
break;
case ColumnCustomType(:final custom):
// Also specify the custom type since it can't be inferred from the
// value passed to the variable.
b
..addText(', ')
..addCode(custom.expression);
}
b.addText(')');
@ -422,6 +426,7 @@ class TextEmitter extends _Node {
TextEmitter(Scope super.parent) : writer = parent.writer;
void write(Object? object) => buffer.write(object);
void writeln(Object? object) => buffer.writeln(object);
void writeUriRef(Uri definition, String element) {

View File

@ -8,7 +8,7 @@ import 'test_utils.dart';
void main() {
test('handles cyclic imports', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/entry.dart': '''
import 'package:drift/drift.dart';
@ -42,7 +42,7 @@ CREATE TABLE bars (
group("reports error when an import can't be found", () {
for (final extension in const ['drift', 'moor']) {
test('in $extension files', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.$extension': '''
import 'b.$extension';
''',
@ -56,7 +56,7 @@ import 'b.$extension';
}
test('in a dart file', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -74,7 +74,7 @@ class Database {
});
test('resolves tables and queries', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/database.dart': r'''
import 'package:drift/drift.dart';
@ -178,7 +178,7 @@ class ProgrammingLanguages extends Table {
});
test('still supports .moor files', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -203,7 +203,7 @@ CREATE TABLE users (
});
test('supports multiple references between same entities', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -231,7 +231,7 @@ class ThisTable extends Table {
});
test('supports references across files', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/this_table.dart': '''
import 'package:drift/drift.dart';
@ -272,7 +272,7 @@ class OtherTable extends Table {
});
test('reports sensible error for missing table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
getCompanyCustomersCount:
SELECT COUNT(*) AS "count"

View File

@ -75,7 +75,7 @@ sqlite:
});
test('reports error about table when module is not imported', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': 'CREATE VIRTUAL TABLE place_spellfix USING spellfix1;',
});

View File

@ -5,7 +5,7 @@ import 'test_utils.dart';
void main() {
test('finds dart expressions', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.drift': '''
import 'foo.dart';
@ -26,7 +26,7 @@ var expr_0 = const MyConverter();
test('only includes direct imports if no Dart expressions are used',
() async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.drift': '''
import 'foo.dart';
import '2.drift';
@ -48,7 +48,7 @@ import 'bar.dart';
});
test('finds nested dart imports', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.drift';
@ -72,7 +72,7 @@ import 'import.dart';
});
test('does not throw for invalid import', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.drift';
import 'does_not_exist.drift';
@ -96,8 +96,8 @@ import 'c.drift';
expect(result.temporaryDartFile, isNot(contains('import')));
});
test('throws if entrypoint does not exist', () {
final backend = TestBackend.inTest({});
test('throws if entrypoint does not exist', () async {
final backend = await TestBackend.inTest({});
expect(
() =>
@ -106,8 +106,8 @@ import 'c.drift';
);
});
test('throws if entrypoint is invalid', () {
final backend = TestBackend.inTest({
test('throws if entrypoint is invalid', () async {
final backend = await TestBackend.inTest({
'a|lib/main.drift': '! this not a valid drift file !',
});

View File

@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
test('analyzes views referencing Dart tables', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/db.dart': '''
import 'package:drift/drift.dart';
import 'dart:io';

View File

@ -5,7 +5,7 @@ import 'test_utils.dart';
void main() {
test('gracefully handles daos with invalid types', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/bar.dart': '''
import 'package:drift/drift.dart';

View File

@ -8,7 +8,7 @@ import '../../test_utils.dart';
void main() {
group('reports a warning', () {
test('when the table is not a class type', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -26,7 +26,7 @@ class Foo extends Table {
});
test('when the column is not a symbol literal', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -48,7 +48,7 @@ class Foo extends Table {
});
test('includes referenced table in database', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -78,7 +78,7 @@ class Database {}
});
test('when the referenced column does not exist', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -106,7 +106,7 @@ class Database {}
});
test('resolves reference', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -149,7 +149,7 @@ class Database {}
});
test('resolves self-references', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -9,7 +9,7 @@ void main() {
test(
'It should rename the table and column name to its snake case version by default',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -36,7 +36,7 @@ class Database {}
test('It should rename the table and column name to its snake case version',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -64,7 +64,7 @@ class Database {}
});
test('It should not rename the table and column name', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -93,7 +93,7 @@ class Database {}
});
test('It should rename the table and column name to its camel case version',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -122,7 +122,7 @@ class Database {}
test(
'It should rename the table and column name to its constant case version',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -150,7 +150,7 @@ class Database {}
});
test('It should rename the table and column name to its pascal case version',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -178,7 +178,7 @@ class Database {}
});
test('It should rename the table and column name to its lower case version',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -207,7 +207,7 @@ class Database {}
});
test('It should rename the table and column name to its upper case version',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -235,7 +235,7 @@ class Database {}
});
test('recognizes custom column types', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -254,13 +254,18 @@ class TestTable extends Table {
final column = table.columns.single;
expect(column.sqlType.builtin, DriftSqlType.any);
expect(column.sqlType.custom?.dartType.toString(), 'List<String>');
expect(column.sqlType.custom?.expression.toString(), 'StringArrayType()');
switch (column.sqlType) {
case ColumnDriftType():
break;
case ColumnCustomType(:final custom):
expect(custom.dartType.toString(), 'List<String>');
expect(custom.expression.toString(), 'StringArrayType()');
}
});
group('customConstraint analysis', () {
test('reports errors', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -279,7 +284,7 @@ class TestTable extends Table {
});
test('resolves foreign key references', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -320,7 +325,7 @@ class TestTable extends Table {
});
test('warns about missing `NOT NULL`', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -339,7 +344,7 @@ class TestTable extends Table {
});
test('applies constraints', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';

View File

@ -7,8 +7,8 @@ import '../../test_utils.dart';
void main() {
late TestBackend state;
setUpAll(() {
state = TestBackend(const {
setUpAll(() async {
state = await TestBackend.init(const {
'a|lib/invalid_no_unnamed_constructor.dart': '''
import 'package:drift/drift.dart';
@ -408,7 +408,7 @@ class Companies extends Table {
});
test('handles `ANY` columns', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'row.dart';
@ -528,7 +528,7 @@ class FooData {
group('records as row types', () {
test('supported with explicit record', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -562,7 +562,7 @@ class Users extends Table {
});
test('supported with implicit record', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.dart': '''
import 'package:drift/drift.dart';

View File

@ -7,7 +7,7 @@ void main() {
final mainUri = Uri.parse('package:a/main.dart');
test('parses schema version getter', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': r'''
import 'package:drift/drift.dart';
@ -27,7 +27,7 @@ class MyDatabase extends _$MyDatabase {
});
test('parses schema version field', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': r'''
import 'package:drift/drift.dart';
@ -47,7 +47,7 @@ class MyDatabase extends _$MyDatabase {
});
test('does not warn about missing tables parameter', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': r'''
import 'package:drift/drift.dart';
@ -69,7 +69,7 @@ class MyDatabase2 extends _$MyDatabase {
});
test('supports inheritance for daos', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/database.dart': r'''
import 'package:drift/drift.dart';
@ -115,7 +115,7 @@ class ProductsDao extends BaseProductsDao with _$ProductDaoMixin {
});
test('only includes duplicate elements once', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -9,7 +9,7 @@ void main() {
late TestBackend backend;
setUpAll(() async {
backend = TestBackend({
backend = await TestBackend.init({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -7,7 +7,7 @@ import '../../test_utils.dart';
void main() {
group('reports a warning', () {
test('when the table is not a class type', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -29,7 +29,7 @@ class Foo extends Table {
});
test('when the table is not a symbol literal', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -54,7 +54,7 @@ class Foo extends Table {
});
test('when the referenced table does not exist', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -83,7 +83,7 @@ class Foo extends Table {
});
test('resolves reference', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -116,7 +116,7 @@ class Foo extends Table {
});
test('resolves self-references', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -5,7 +5,7 @@ import '../../test_utils.dart';
void main() {
test('resolves index', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -41,7 +41,7 @@ class MyTable extends Table {
});
test('warns about missing columns', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';

View File

@ -7,7 +7,7 @@ import '../../test_utils.dart';
void main() {
test('warns about invalid column', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -4,7 +4,7 @@ import '../../test_utils.dart';
void main() {
test('can define abstract tables', () async {
final test = TestBackend.inTest({
final test = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -11,8 +11,8 @@ void main() {
late TestBackend backend;
late FileState state;
setUpAll(() {
backend = TestBackend({
setUpAll(() async {
backend = await TestBackend.init({
'a|lib/main.dart': r'''
import 'package:drift/drift.dart';

View File

@ -8,8 +8,8 @@ import '../../test_utils.dart';
void main() {
late TestBackend backend;
setUpAll(() {
backend = TestBackend({
setUpAll(() async {
backend = await TestBackend.init({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -335,7 +335,7 @@ class Pianos extends Table {
});
test('reads custom constraints from table', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -377,7 +377,7 @@ class WithConstraints extends Table {
});
test('warns about foreign key references from customConstraints', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -410,7 +410,7 @@ class WithConstraints extends Table {
});
test('can resolve references from import', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/topic.dart': '''
import 'package:drift/drift.dart';
@ -452,7 +452,7 @@ class Videos extends Table {
});
test('supports autoIncrement on int64 columns', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';

View File

@ -6,8 +6,8 @@ import '../../test_utils.dart';
void main() {
late TestBackend state;
setUp(() {
state = TestBackend({
setUp(() async {
state = await TestBackend.init({
'a|lib/json.dart': '''
import 'package:drift/drift.dart';

View File

@ -7,7 +7,7 @@ void main() {
final mainUri = Uri.parse('package:a/main.dart');
test('does not allow autoIncrement() to have a unique constraint', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -29,7 +29,7 @@ class Test extends Table {
});
test('does not allow primary key to have a unique constraint', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -55,7 +55,7 @@ class Test extends Table {
test(
'does not allow primary key to have a unique constraint through override',
() async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -84,7 +84,7 @@ class Test extends Table {
});
test('warns about duplicate unique declarations', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@ -108,7 +108,7 @@ class Test extends Table {
});
test('parses unique key definitions', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -6,7 +6,7 @@ import '../../test_utils.dart';
void main() {
test('can analyze Dart view', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';

View File

@ -8,7 +8,7 @@ import '../test_utils.dart';
void main() {
group('drift files', () {
test('finds local elements', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE foo (bar INTEGER);
@ -41,7 +41,7 @@ CREATE VIEW my_view AS SELECT whatever FROM unknown_table;
});
test('reports syntax errors', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE valid_1 (bar INTEGER);
@ -64,7 +64,7 @@ CREATE TABLE valid_2 (bar INTEGER);
});
test('warns about duplicate elements', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE a (id INTEGER);
CREATE VIEW a AS VALUES(1,2,3);
@ -82,7 +82,7 @@ CREATE VIEW a AS VALUES(1,2,3);
group('imports', () {
test('are resolved', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': "import 'b.drift';",
'a|lib/b.drift': "CREATE TABLE foo (bar INTEGER);",
});
@ -106,7 +106,7 @@ CREATE VIEW a AS VALUES(1,2,3);
});
test('can handle circular imports', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': "import 'a.drift'; import 'b.drift';",
'a|lib/b.drift': "import 'a.drift';",
});
@ -119,7 +119,7 @@ CREATE VIEW a AS VALUES(1,2,3);
group('dart files', () {
test('fails for part files', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
part of 'b.dart';
''',
@ -136,7 +136,7 @@ part 'a.dart';
});
test('finds tables', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -172,7 +172,7 @@ class Groups extends Table {
});
test('ignores abstract tables', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@ -208,7 +208,7 @@ abstract class BaseRelationTable extends Table {
});
test('table name errors', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/expr.dart': '''
import 'package:drift/drift.dart';
@ -239,7 +239,7 @@ class InvalidGetter extends Table {
});
test('warns about duplicate elements', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';

View File

@ -8,7 +8,7 @@ import '../../test_utils.dart';
void main() {
test('view created', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/table.drift': '''
CREATE TABLE t (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL);
''',
@ -34,7 +34,7 @@ void main() {
});
test('view created from another view', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/table.drift': '''
CREATE TABLE t (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL);
''',
@ -68,7 +68,7 @@ void main() {
});
test('view without table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE VIEW random_view AS
SELECT name FROM t WHERE id % 2 = 0;
@ -82,7 +82,7 @@ void main() {
});
test('does not allow nested columns', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE foo (bar INTEGER NOT NULL PRIMARY KEY);
@ -102,7 +102,7 @@ void main() {
test('imported views are analyzed', () async {
// Regression test for https://github.com/simolus3/drift/issues/1639
final testState = TestBackend.inTest({
final testState = await TestBackend.inTest({
'a|lib/imported.drift': '''
CREATE TABLE a (
b TEXT NOT NULL
@ -124,7 +124,7 @@ query: SELECT * FROM my_view;
});
test('picks valid Dart names for columns', () async {
final testState = TestBackend.inTest({
final testState = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE VIEW IF NOT EXISTS repro AS
SELECT 1,
@ -148,7 +148,7 @@ CREATE VIEW IF NOT EXISTS repro AS
});
test('copies type converter from table', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'converter.dart';
@ -185,7 +185,7 @@ TypeConverter<Object, int> createConverter() => throw UnimplementedError();
});
test('can declare type converter on view column', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'converter.dart';
@ -223,7 +223,7 @@ TypeConverter<Object, int> createConverter() => throw UnimplementedError();
});
test('supports enum columns', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enums.dart';
@ -280,7 +280,7 @@ enum MyEnum {
String expectedSql,
DriftOptions options,
) async {
final backend = TestBackend.inTest(
final backend = await TestBackend.inTest(
{'a|lib/a.drift': definition},
options: options,
);

View File

@ -6,7 +6,7 @@ import '../../test_utils.dart';
void main() {
test('parse nested CTE', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/test.drift': '''
test:
SELECT
@ -38,7 +38,7 @@ SELECT
});
test('recognizes CTE clause', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/test.drift': '''
test:
WITH RECURSIVE
@ -70,7 +70,7 @@ WITH RECURSIVE
});
test('finds the underlying table when aliased through CTE', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/test.drift': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,

View File

@ -5,7 +5,7 @@ import '../../test_utils.dart';
void main() {
test('can use existing row classes in drift files', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/db.drift': '''
import 'rows.dart';
@ -61,7 +61,7 @@ class ExistingForView {
});
test('can use generic row classes', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/generic.dart': '''
//@dart=2.13
typedef StringRow = GenericRow<String>;
@ -105,7 +105,7 @@ CREATE TABLE drift_ints (
group('can use records', () {
test('with explicit structure', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'helper.dart';
@ -135,7 +135,7 @@ typedef MyRecord = ({String foo, int? bar});
});
test('implicitly', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE foo (
foo TEXT NOT NULL,

View File

@ -5,7 +5,7 @@ import '../../test_utils.dart';
void main() {
test('reports an error when importing a part file into .drift', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/base.dart': '''
import 'package:drift/drift.dart';

View File

@ -84,7 +84,7 @@ void main() {
test('integration tests with drift files and experimental inference',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
const {
'foo|lib/a.drift': '''
CREATE TABLE numbers (foo REAL NOT NULL);

View File

@ -8,7 +8,7 @@ const _options = DriftOptions.defaults(modules: [SqlModule.fts5]);
void main() {
group('reports error', () {
test('for missing content table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE VIRTUAL TABLE fts USING fts5(a, c, content=tbl);
''',
@ -23,7 +23,7 @@ CREATE VIRTUAL TABLE fts USING fts5(a, c, content=tbl);
});
test('for invalid rowid of content table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE tbl (a, b, c, my_pk INTEGER PRIMARY KEY);
@ -39,7 +39,7 @@ CREATE VIRTUAL TABLE fts USING fts5(a, c, content=tbl, content_rowid=d);
});
test('when referencing an unknown column', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE tbl (a, b, c, d INTEGER PRIMARY KEY);
@ -54,7 +54,7 @@ CREATE VIRTUAL TABLE fts USING fts5(e, c, content=tbl, content_rowid=d);
});
test('finds referenced table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE tbl (a, b, c, d INTEGER PRIMARY KEY);

View File

@ -5,7 +5,7 @@ import '../../test_utils.dart';
void main() {
test('drift files can import original dart source', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/base.dart': r'''
import 'package:drift/drift.dart';

View File

@ -8,7 +8,7 @@ import '../../test_utils.dart';
void main() {
// https://github.com/simolus3/drift/issues/2097#issuecomment-1273008383
test('virtual columns are not required for inserts', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'foo|lib/a.drift': r'''
CREATE TABLE IF NOT EXISTS nodes (

View File

@ -6,7 +6,7 @@ import '../../test_utils.dart';
void main() {
// Regression test for https://github.com/simolus3/drift/issues/754
test('supports fts5 tables with external content', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b TEXT, c TEXT);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');

View File

@ -20,7 +20,7 @@ query: INSERT INTO foo VALUES (?, ?, ?)
void main() {
test('does not support newer sqlite features by default', () async {
final state = TestBackend.inTest(_content);
final state = await TestBackend.inTest(_content);
final file = await state.analyze('package:a/main.drift');
expect(
@ -38,7 +38,7 @@ void main() {
});
test('supports newer sqlite features', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
_content,
options: const DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(

View File

@ -14,7 +14,7 @@ enum Fruit {
void main() {
group('warns about invalid type converter value', () {
test('in table definition', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enum.dart';
@ -38,7 +38,7 @@ CREATE TABLE a (
});
test('for query', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enum.dart';

View File

@ -1,13 +1,14 @@
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift_dev/src/analysis/results/column.dart';
import 'package:drift_dev/src/analysis/results/table.dart';
import 'package:drift_dev/src/analysis/results/types.dart';
import 'package:test/test.dart';
import '../../test_utils.dart';
void main() {
test('reports foreign keys in drift model', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE a (
foo INTEGER PRIMARY KEY,
@ -55,7 +56,7 @@ CREATE TABLE b (
});
test('recognizes aliases to rowid', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE users (
id INTEGER PRIMARY KEY,
@ -83,7 +84,7 @@ CREATE TABLE b (
});
test('parses enum columns', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enum.dart';
@ -167,7 +168,7 @@ CREATE TABLE b (
});
test('does not allow converters for enum columns', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enum.dart';
@ -199,7 +200,7 @@ CREATE TABLE b (
});
test('does not allow enum types for non-enums', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enum.dart';
@ -222,7 +223,7 @@ CREATE TABLE b (
});
test('supports JSON KEY annotation', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE waybills (
parent INT JSON KEY parentDoc NULL,
@ -243,7 +244,7 @@ CREATE TABLE waybills (
});
test('recognizes documentation comments', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE IF NOT EXISTS currencies (
-- The name of this currency
@ -265,7 +266,7 @@ CREATE TABLE IF NOT EXISTS currencies (
});
test('can use custom types', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.dart';
@ -286,13 +287,17 @@ class MyType implements CustomSqlType<String> {}
final table = file.analyzedElements.single as DriftTable;
final column = table.columns.single;
expect(column.sqlType.isCustom, isTrue);
expect(column.sqlType.custom?.dartType.toString(), 'String');
expect(column.sqlType.custom?.expression.toString(), 'MyType()');
switch (column.sqlType) {
case ColumnDriftType():
fail('expect custom type');
case ColumnCustomType(:final custom):
expect(custom.dartType.toString(), 'String');
expect(custom.expression.toString(), 'MyType()');
}
});
test('recognizes bigint columns', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE foo (
bar INT64 NOT NULL

View File

@ -7,7 +7,7 @@ import '../../test_utils.dart';
void main() {
test('supports virtual tables across drift files', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/table.drift': '''
CREATE TABLE example_table (
@ -43,7 +43,7 @@ exampleSearch: SELECT example_table.**, s.* FROM example_table
});
test('query virtual tables with unknown function', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/table.drift': '''
CREATE TABLE example_table (
@ -72,7 +72,7 @@ SELECT rowid, highlight(example_table_search, 0, '[match]', '[match]') name,
});
test('supports spellfix1 tables', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{'a|lib/a.drift': 'CREATE VIRTUAL TABLE demo USING spellfix1;'},
options: DriftOptions.defaults(
dialect: DialectOptions(

View File

@ -8,7 +8,7 @@ import '../../test_utils.dart';
void main() {
Future<Iterable<SqlQuery>> analyzeQueries(String driftFile) async {
final state = TestBackend.inTest({'a|lib/a.drift': driftFile});
final state = await TestBackend.inTest({'a|lib/a.drift': driftFile});
final result = await state.analyze('package:a/a.drift');
return result.fileAnalysis!.resolvedQueries.values;

View File

@ -8,7 +8,7 @@ import 'utils.dart';
void main() {
test('recognizes existing row classes', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -35,7 +35,7 @@ class MyRow {
});
test('can use named constructors', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -75,7 +75,7 @@ class MyRow {
});
test("warns if existing row classes don't exist", () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -91,7 +91,7 @@ foo WITH MyRow: SELECT 'hello world', 2;
});
test('resolves existing row class', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -122,7 +122,7 @@ class MyRow {
group('matches', () {
test('single column type', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
foo WITH int: SELECT 1 AS r;
''',
@ -139,7 +139,7 @@ foo WITH int: SELECT 1 AS r;
});
test('single table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -166,7 +166,7 @@ typedef MyRow = TblData;
});
test('single table with custom row class', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -195,7 +195,7 @@ class MyTableRow {
});
test('alternative to table class', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -229,7 +229,7 @@ class MyQueryRow {
group('nested column', () {
test('single column into field', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -263,7 +263,7 @@ class MyQueryRow {
});
test('single column into single-element record', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -298,7 +298,7 @@ class MyQueryRow {
});
test('custom result set', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -344,7 +344,7 @@ class JsonStructure {
});
test('table', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -382,7 +382,7 @@ class MyRow {
});
test('table as alternative to row class', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -425,7 +425,7 @@ class MyRow {
group('nested LIST query', () {
test('single column type', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -461,7 +461,7 @@ class MyQueryRow {
});
test('custom result set with class', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -503,7 +503,7 @@ class MyNestedTable {
});
test('custom result set with record', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -545,7 +545,7 @@ class MyRow {
});
test('into record', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -586,7 +586,7 @@ typedef MyRow = (int, List<TblData>);
test(
'default record',
() async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -625,7 +625,7 @@ foo WITH Record: SELECT 1 AS a, LIST(SELECT * FROM tbl) AS b FROM tbl;
);
test('mix', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -685,7 +685,7 @@ class MyRow {
group('error', () {
test('when the specified class has no default constructor', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -705,7 +705,7 @@ class MyRow {
});
test('when the desired constructor does not exist', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -725,7 +725,7 @@ class MyRow {
});
test('when there is a parameter with no matching column', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -745,7 +745,7 @@ class MyRow {
});
test('when a record has too many positional fields', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -767,7 +767,7 @@ typedef MyRow = (int, String, DateTime);
});
test('when a record has an unmatched named field', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
@ -788,7 +788,7 @@ typedef MyRow = (int, {String d});
});
test('when there is a type mismatch on a scalar column', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -808,7 +808,7 @@ class MyRow {
});
test('when a list column is not a list', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -832,7 +832,7 @@ class MyRow {
test(
'when there is a type mismatch on a nested scalar column',
() async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';

View File

@ -6,7 +6,7 @@ import '../../test_utils.dart';
void main() {
test('experimental inference - integration test', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE artists (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

View File

@ -76,7 +76,7 @@ q: SELECT * FROM t WHERE i IN ?1;
});
test('warns about default values outside of expressions', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': r'''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY,
@ -96,7 +96,7 @@ all ($limit = 3): SELECT * FROM foo LIMIT $limit;
});
test('warns when placeholder are used in insert with columns', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': r'''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY,
@ -118,7 +118,7 @@ in: INSERT INTO foo (id) $placeholder;
test(
'warns when nested results appear in compound statements',
() async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY,
@ -142,7 +142,7 @@ all: SELECT foo.** FROM foo UNION ALL SELECT foo.** FROM foo;
test(
'warns when nested query appear in nested query',
() async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY,
@ -175,7 +175,7 @@ all: SELECT foo.**, LIST(SELECT *, LIST(SELECT * FROM foo) FROM foo) FROM foo;
}
test('in top-level queries', () async {
state = TestBackend.inTest({
state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -189,7 +189,7 @@ test: INSERT INTO foo VALUES (?)
});
test('in CREATE TRIGGER statements', () async {
state = TestBackend.inTest({
state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -205,7 +205,7 @@ END;
});
test('in @create statements', () async {
state = TestBackend.inTest({
state = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,

View File

@ -5,7 +5,7 @@ import '../../test_utils.dart';
void main() {
test('select from view', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'foo|lib/a.drift': '''
CREATE TABLE artists (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

View File

@ -9,7 +9,7 @@ import 'utils.dart';
void main() {
test('respects explicit type arguments', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': '''
bar(?1 AS TEXT, :foo AS BOOLEAN): SELECT ?, :foo;
''',
@ -29,7 +29,7 @@ bar(?1 AS TEXT, :foo AS BOOLEAN): SELECT ?, :foo;
});
test('can read from builtin tables', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/main.drift': '''
testQuery: SELECT * FROM sqlite_schema;
''',
@ -43,7 +43,7 @@ testQuery: SELECT * FROM sqlite_schema;
});
test('reads REQUIRED syntax', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': '''
bar(REQUIRED ?1 AS TEXT OR NULL, REQUIRED :foo AS BOOLEAN): SELECT ?, :foo;
''',
@ -64,7 +64,7 @@ bar(REQUIRED ?1 AS TEXT OR NULL, REQUIRED :foo AS BOOLEAN): SELECT ?, :foo;
});
test('infers result set for views', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': r'''
CREATE VIEW my_view AS SELECT 'foo', 2;
@ -90,7 +90,7 @@ query: SELECT * FROM my_view;
});
test('infers nested result set for views', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': r'''
CREATE VIEW my_view AS SELECT 'foo', 2;
@ -123,7 +123,7 @@ query: SELECT foo.**, bar.** FROM my_view foo, my_view bar;
});
test('infers nested result sets for custom result sets', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': r'''
query: SELECT 1 AS a, b.** FROM (SELECT 2 AS b, 3 AS c) AS b;
''',
@ -154,7 +154,7 @@ query: SELECT 1 AS a, b.** FROM (SELECT 2 AS b, 3 AS c) AS b;
for (final dateTimeAsText in [false, true]) {
test('analyzing date times (stored as text: $dateTimeAsText)', () async {
final state = TestBackend.inTest(
final state = await TestBackend.inTest(
{
'foo|lib/foo.drift': r'''
CREATE TABLE foo (
@ -202,7 +202,7 @@ q3: SELECT datetime('now');
}
test('resolves nested result sets', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': r'''
CREATE TABLE points (
id INTEGER NOT NULL PRIMARY KEY,
@ -243,7 +243,7 @@ FROM routes
});
test('resolves nullability of aliases in nested result sets', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'foo|lib/main.drift': r'''
CREATE TABLE tableA1 (id INTEGER);
CREATE TABLE tableB1 (id INTEGER);
@ -285,7 +285,7 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
test('supports custom functions', () async {
final withoutOptions =
TestBackend.inTest({'a|lib/a.drift': 'a: SELECT my_function();'});
await TestBackend.inTest({'a|lib/a.drift': 'a: SELECT my_function();'});
var result = await withoutOptions.analyze('package:a/a.drift');
expect(result.allErrors, [
isDriftError('Function my_function could not be found')
@ -294,14 +294,13 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
.withSpan('my_function()'),
]);
final withOptions =
TestBackend.inTest({'a|lib/a.drift': 'a: SELECT my_function(?, ?);'},
options: DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(knownFunctions: {
'my_function':
KnownSqliteFunction.fromJson('boolean (int, text)')
}),
));
final withOptions = await TestBackend.inTest(
{'a|lib/a.drift': 'a: SELECT my_function(?, ?);'},
options: DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(knownFunctions: {
'my_function': KnownSqliteFunction.fromJson('boolean (int, text)')
}),
));
result = await withOptions.analyze('package:a/a.drift');
withOptions.expectNoErrors();
@ -318,7 +317,7 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
});
test('can cast to DATETIME and BOOLEAN', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
a: SELECT CAST(1 AS BOOLEAN) AS a, CAST(2 AS DATETIME) as b;
''',
@ -337,7 +336,7 @@ a: SELECT CAST(1 AS BOOLEAN) AS a, CAST(2 AS DATETIME) as b;
});
test('can cast to enum type', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'enum.dart';

View File

@ -7,7 +7,7 @@ import '../test_utils.dart';
void main() {
group('from clean state', () {
test('resolves simple tables', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE a (
foo INTEGER PRIMARY KEY,
@ -42,7 +42,7 @@ CREATE TABLE b (
group('references', () {
test('self', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE a (
foo INTEGER PRIMARY KEY,
@ -61,7 +61,7 @@ CREATE TABLE a (
});
test('across files', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.drift';
@ -93,7 +93,7 @@ CREATE TABLE b (
});
test('for triggers', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.drift';
@ -133,7 +133,7 @@ CREATE TABLE deleted_b (
group('non-existing', () {
test('from table', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE a (
foo INTEGER PRIMARY KEY,
@ -151,7 +151,7 @@ CREATE TABLE a (
[isDriftError('`b` could not be found in any import.')]);
});
test('in a trigger', () async {
final backend = TestBackend.inTest(const {
final backend = await TestBackend.inTest(const {
'foo|lib/a.drift': '''
CREATE TRIGGER IF NOT EXISTS foo BEFORE DELETE ON bar BEGIN
END;
@ -172,7 +172,7 @@ END;
});
test('emits warning on invalid import', () async {
final backend = TestBackend.inTest({
final backend = await TestBackend.inTest({
'a|lib/a.drift': "import 'b.drift';",
});

View File

@ -8,7 +8,7 @@ import '../test_utils.dart';
void main() {
late TestBackend tester;
setUpAll(() => tester = TestBackend({}));
setUpAll(() async => tester = await TestBackend.init({}));
tearDownAll(() => tester.dispose());
group('from AST', () {

View File

@ -37,24 +37,43 @@ class TestBackend extends DriftBackend {
AnalysisContext? _dartContext;
OverlayResourceProvider? _resourceProvider;
TestBackend(
TestBackend._(
Map<String, String> sourceContents, {
DriftOptions options = const DriftOptions.defaults(),
this.analyzerExperiments = const Iterable.empty(),
}) : sourceContents = {
for (final entry in sourceContents.entries)
AssetId.parse(entry.key).uri.toString(): entry.value,
} {
driver = DriftAnalysisDriver(this, options, isTesting: true);
}
};
factory TestBackend.inTest(
static Future<TestBackend> init(
Map<String, String> sourceContents, {
DriftOptions options = const DriftOptions.defaults(),
Iterable<String> analyzerExperiments = const Iterable.empty(),
}) {
final backend = TestBackend(sourceContents,
options: options, analyzerExperiments: analyzerExperiments);
}) async {
final backend = TestBackend._(
sourceContents,
options: options,
analyzerExperiments: analyzerExperiments,
);
backend.driver =
await DriftAnalysisDriver.init(backend, options, isTesting: true);
return backend;
}
static Future<TestBackend> inTest(
Map<String, String> sourceContents, {
DriftOptions options = const DriftOptions.defaults(),
Iterable<String> analyzerExperiments = const Iterable.empty(),
}) async {
final backend = await TestBackend.init(
sourceContents,
options: options,
analyzerExperiments: analyzerExperiments,
);
addTearDown(backend.dispose);
return backend;
@ -62,9 +81,10 @@ class TestBackend extends DriftBackend {
static Future<FileState> analyzeSingle(String content,
{String asset = 'a|lib/a.drift',
DriftOptions options = const DriftOptions.defaults()}) {
DriftOptions options = const DriftOptions.defaults()}) async {
final assetId = AssetId.parse(asset);
final backend = TestBackend.inTest({asset: content}, options: options);
final backend =
await TestBackend.inTest({asset: content}, options: options);
return backend.driver.fullyAnalyze(assetId.uri);
}
@ -217,6 +237,9 @@ class TestBackend extends DriftBackend {
return null;
}
@override
bool get canReadDart => true;
@override
Future<LibraryElement> readDart(Uri uri) async {
await ensureHasDartAnalyzer();

View File

@ -578,11 +578,15 @@ 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,
),
);
@ -606,7 +610,8 @@ class MyDatabase {
// However, the discover builder should not read other drift files.
for (final input in inputs.keys) {
if (input.endsWith('.drift')) {
expectReadsForBuilder(input, DriftDiscover, [makeAssetId(input)]);
expectReadsForBuilder(input, DriftDiscover,
everyElement(anyOf(makeAssetId(input), isFromExternalPackage)));
} else {
expectReadsForBuilder(
input,

View File

@ -7,7 +7,7 @@ import '../analysis/test_utils.dart';
void main() {
test('finds update rules for triggers', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
@ -51,7 +51,7 @@ class MyDatabase {}
});
test('finds update rules for foreign key constraint', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE a (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

View File

@ -95,7 +95,7 @@ CREATE VIEW user_ids AS SELECT id FROM users;
}
Future<List<DriftElement>> _analyzeAndSerialize(String source) async {
final state = TestBackend.inTest({'a|lib/a.drift': source});
final state = await TestBackend.inTest({'a|lib/a.drift': source});
final file = await state.analyze('package:a/a.drift');
final writer = SchemaWriter(file.analyzedElements.toList());

View File

@ -14,7 +14,7 @@ import '../../analysis/test_utils.dart';
void main() {
test('writer integration test', () async {
final state = TestBackend.inTest({
final state = await TestBackend.inTest({
'a|lib/a.drift': '''
import 'main.dart';

View File

@ -15,8 +15,8 @@ void main() {
{DriftOptions options = const DriftOptions.defaults(
generateNamedParameters: true,
)}) async {
final state =
TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options);
final state = await TestBackend.inTest({'a|lib/main.drift': driftFile},
options: options);
final file = await state.analyze('package:a/main.drift');
state.expectNoErrors();

View File

@ -5,6 +5,7 @@ export 'src/analysis/analysis.dart';
export 'src/analysis/types/join_analysis.dart';
export 'src/ast/ast.dart';
export 'src/engine/module/fts5.dart' show Fts5Extension, Fts5Table;
export 'src/engine/module/geopoly.dart' show GeopolyExtension;
export 'src/engine/module/json1.dart' show Json1Extension;
export 'src/engine/module/math.dart' show BuiltInMathExtension;
export 'src/engine/module/rtree.dart' show RTreeExtension;

View File

@ -41,6 +41,7 @@ class ResolvedType {
this.hints = const [],
this.nullable = false,
this.isArray = false});
const ResolvedType.bool({bool? nullable = false})
: this(
type: BasicType.int,
@ -111,6 +112,7 @@ abstract class TypeHint {
@override
int get hashCode => runtimeType.hashCode;
@override
bool operator ==(Object other) => other.runtimeType == runtimeType;
}
@ -131,6 +133,12 @@ class IsBigInt extends TypeHint {
const IsBigInt();
}
/// This could be a `blob` or `text` depending on the context
/// https://www.sqlite.org/geopoly.html
class IsGeopolyPolygon extends TypeHint {
const IsGeopolyPolygon();
}
/// Result of resolving a type. This can either have the resolved [type] set,
/// or it can inform the called that it [needsContext] to resolve the type
/// properly. Failure to resolve the type will have the [unknown] flag set.
@ -152,10 +160,12 @@ class ResolveResult {
const ResolveResult(this.type)
: needsContext = false,
unknown = false;
const ResolveResult.needsContext()
: type = null,
needsContext = true,
unknown = false;
const ResolveResult.unknown()
: type = null,
needsContext = false,

View File

@ -0,0 +1,292 @@
import 'package:sqlparser/src/analysis/analysis.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/engine/module/module.dart';
import 'package:sqlparser/src/engine/sql_engine.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
final class GeopolyExtension implements Extension {
const GeopolyExtension();
@override
void register(SqlEngine engine) {
engine
..registerModule(_GeopolyModule(engine))
..registerFunctionHandler(_GeopolyFunctionHandler());
}
}
const String _shapeKeyword = '_shape';
const ResolvedType _typePolygon = ResolvedType(
type: BasicType.blob,
nullable: true,
hints: [
IsGeopolyPolygon(),
],
);
final class _GeopolyModule extends Module {
_GeopolyModule(this.engine) : super('geopoly');
final SqlEngine engine;
@override
Table parseTable(CreateVirtualTableStatement stmt) {
final resolvedColumns = <TableColumn>[
RowId(),
TableColumn(
_shapeKeyword,
_typePolygon,
),
];
for (final column in stmt.argumentContent) {
final tokens = engine.tokenize(column);
final String resolvedName;
final ResolvedType resolvedType;
switch (tokens) {
// geoID INTEGER not null
case [final name, final type, final not, final $null, final eof]
when name.type == TokenType.identifier &&
type.type == TokenType.identifier &&
not.type == TokenType.not &&
$null.type == TokenType.$null &&
eof.type == TokenType.eof:
resolvedName = name.lexeme;
resolvedType = engine.schemaReader
.resolveColumnType(type.lexeme)
.withNullable(false);
// a INTEGER
case [final name, final type, final eof]
when name.type == TokenType.identifier &&
type.type == TokenType.identifier &&
eof.type == TokenType.eof:
resolvedName = name.lexeme;
resolvedType = engine.schemaReader
.resolveColumnType(type.lexeme)
.withNullable(true);
// b
case [final name, final eof]
when name.type == TokenType.identifier && eof.type == TokenType.eof:
resolvedName = name.lexeme;
resolvedType = const ResolvedType(
type: BasicType.any,
nullable: true,
);
// ?
default:
throw ArgumentError('Can\'t be parsed', column);
}
resolvedColumns.add(
TableColumn(
resolvedName,
resolvedType,
),
);
}
return Table(
name: stmt.tableName,
resolvedColumns: resolvedColumns,
definition: stmt,
isVirtual: true,
);
}
}
final class _GeopolyFunctionHandler extends FunctionHandler {
@override
Set<String> get functionNames => {
for (final value in _GeopolyFunctions.values) value.sqlName,
};
@override
ResolveResult inferArgumentType(
AnalysisContext context,
SqlInvocation call,
Expression argument,
) {
// TODO(nikitadol): Copy from `_Fts5Functions`. Must be removed when argument index appears
int? argumentIndex;
if (call.parameters is ExprFunctionParameters) {
argumentIndex = (call.parameters as ExprFunctionParameters)
.parameters
.indexOf(argument);
}
if (argumentIndex == null || argumentIndex < 0) {
// couldn't find expression in arguments, so we don't know the type
return const ResolveResult.unknown();
}
//
final func = _GeopolyFunctions.bySqlName(call.name);
if (argumentIndex < func.args.length) {
return ResolveResult(func.args[argumentIndex]);
} else if (func.otherArgs != null) {
return ResolveResult(func.otherArgs);
} else {
return ResolveResult.unknown();
}
}
@override
ResolveResult inferReturnType(
AnalysisContext context,
SqlInvocation call,
List<Typeable> expandedArgs,
) {
final func = _GeopolyFunctions.bySqlName(call.name);
if (expandedArgs.length == func.args.length) {
// ok
} else if (expandedArgs.length > func.args.length &&
func.otherArgs != null) {
// ok
} else {
final buffer = StringBuffer(
'The function `${func.sqlName}` takes ',
);
buffer.write('${func.args.length} ');
switch (func.args.length) {
case 1:
buffer.write('argument');
case > 1:
buffer.write('arguments');
}
if (func.otherArgs != null) {
buffer.write(' (or more)');
}
buffer.write(' but ${expandedArgs.length} ');
switch (expandedArgs.length) {
case 1:
buffer.write('argument is');
case > 1:
buffer.write('arguments are');
}
buffer.write('passed');
throw ArgumentError(buffer);
}
return ResolveResult(
func.returnType,
);
}
}
const _typeInt = ResolvedType(
type: BasicType.int,
nullable: true,
);
const _typeReal = ResolvedType(
type: BasicType.real,
nullable: true,
);
const _typeBlob = ResolvedType(
type: BasicType.blob,
nullable: true,
);
const _typeText = ResolvedType(
type: BasicType.text,
nullable: true,
);
enum _GeopolyFunctions {
overlap(
'geopoly_overlap',
_typeInt,
[_typePolygon, _typePolygon],
),
within(
'geopoly_within',
_typeInt,
[_typePolygon, _typePolygon],
),
area(
'geopoly_area',
_typeReal,
[_typePolygon],
),
blob(
'geopoly_blob',
_typeBlob,
[_typePolygon],
),
json(
'geopoly_json',
_typeText,
[_typePolygon],
),
svg(
'geopoly_svg',
_typeText,
[_typePolygon],
_typeText,
),
bbox(
'geopoly_bbox',
_typeBlob,
[_typePolygon],
),
groupBbox(
'geopoly_group_bbox',
_typeBlob,
[_typePolygon],
),
containsPoint(
'geopoly_contains_point',
_typeInt,
[_typePolygon, _typeInt, _typeInt],
),
xform(
'geopoly_xform',
_typeBlob,
[
_typePolygon,
_typeReal,
_typeReal,
_typeReal,
_typeReal,
_typeReal,
_typeReal
],
),
regular(
'geopoly_regular',
_typeBlob,
[_typeReal, _typeReal, _typeReal, _typeInt],
),
ccw(
'geopoly_ccw',
_typeBlob,
[_typePolygon],
);
final String sqlName;
final ResolvedType returnType;
final List<ResolvedType> args;
final ResolvedType? otherArgs;
const _GeopolyFunctions(
this.sqlName,
this.returnType,
this.args, [
this.otherArgs,
]);
factory _GeopolyFunctions.bySqlName(String sqlName) {
return _GeopolyFunctions.values.firstWhere(
(element) => element.sqlName == sqlName,
orElse: () => throw ArgumentError('$sqlName not exists'));
}
}

View File

@ -0,0 +1,30 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
final _geopolyOptions = EngineOptions(
enabledExtensions: const [
GeopolyExtension(),
],
);
void main() {
group('creating geopoly tables', () {
final engine = SqlEngine(_geopolyOptions);
test('can create geopoly table', () {
final result = engine.analyze(
'''CREATE VIRTUAL TABLE geo USING geopoly(a integer not null, b integer, c);''');
final table = const SchemaFromCreateTable()
.read(result.root as TableInducingStatement);
expect(table.name, 'geo');
final columns = table.resultColumns;
expect(columns, hasLength(4));
expect(columns[0].type.type, equals(BasicType.blob));
expect(columns[1].type.type, equals(BasicType.int));
expect(columns[2].type.type, equals(BasicType.int));
expect(columns[3].type.type, equals(BasicType.any));
});
});
}