From efc5a6c985bd43ea2baaa51f93ac7d6fde82d1c0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 22 Apr 2024 22:51:07 +0200 Subject: [PATCH] More docs, integration tests --- docs/pages/docs/Generation options/index.md | 2 + drift/build.yaml | 2 + drift/lib/extensions/geopoly.dart | 24 +- drift/test/extensions/geopoly.drift | 3 + .../extensions/geopoly_integration_test.dart | 49 ++++ .../geopoly_integration_test.g.dart | 226 ++++++++++++++++++ drift/test/test_utils/test_utils.mocks.dart | 2 +- .../build/build_integration_test.dart | 2 + 8 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 drift/test/extensions/geopoly.drift create mode 100644 drift/test/extensions/geopoly_integration_test.dart create mode 100644 drift/test/extensions/geopoly_integration_test.g.dart diff --git a/docs/pages/docs/Generation options/index.md b/docs/pages/docs/Generation options/index.md index 0246caa1..50d836ec 100644 --- a/docs/pages/docs/Generation options/index.md +++ b/docs/pages/docs/Generation options/index.md @@ -179,6 +179,8 @@ We currently support the following extensions: - `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. +- [geopoly](https://www.sqlite.org/geopoly.html), a generalization of the R*Tree module supporting more complex + polygons. - `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). diff --git a/drift/build.yaml b/drift/build.yaml index 52b9a734..1aa5be5e 100644 --- a/drift/build.yaml +++ b/drift/build.yaml @@ -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 diff --git a/drift/lib/extensions/geopoly.dart b/drift/lib/extensions/geopoly.dart index fbc401aa..4bd104a3 100644 --- a/drift/lib/extensions/geopoly.dart +++ b/drift/lib/extensions/geopoly.dart @@ -8,9 +8,13 @@ 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 { - /// + /// Default constant constructor for the geopoly type. const GeopolyPolygonType(); @override @@ -43,29 +47,33 @@ final class GeopolyPolygonType implements CustomSqlType { } } -/// In Geopoly, a polygon can be text or a blob +/// 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._(); } diff --git a/drift/test/extensions/geopoly.drift b/drift/test/extensions/geopoly.drift new file mode 100644 index 00000000..de46f95d --- /dev/null +++ b/drift/test/extensions/geopoly.drift @@ -0,0 +1,3 @@ +CREATE VIRTUAL TABLE geopoly_test USING geopoly(a); + +area: SELECT geopoly_area(_shape) FROM geopoly_test WHERE rowid = ?; diff --git a/drift/test/extensions/geopoly_integration_test.dart b/drift/test/extensions/geopoly_integration_test.dart new file mode 100644 index 00000000..1a029029 --- /dev/null +++ b/drift/test/extensions/geopoly_integration_test.dart @@ -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()); + + 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; +} diff --git a/drift/test/extensions/geopoly_integration_test.g.dart b/drift/test/extensions/geopoly_integration_test.g.dart new file mode 100644 index 00000000..a307c839 --- /dev/null +++ b/drift/test/extensions/geopoly_integration_test.g.dart @@ -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, + VirtualTableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GeopolyTest(this.attachedDatabase, [this._alias]); + static const VerificationMeta _shapeMeta = const VerificationMeta('shape'); + late final GeneratedColumn shape = + GeneratedColumn('_shape', aliasedName, true, + type: const GeopolyPolygonType(), + requiredDuringInsert: false, + $customConstraints: ''); + static const VerificationMeta _aMeta = const VerificationMeta('a'); + late final GeneratedColumn a = GeneratedColumn( + 'a', aliasedName, true, + type: DriftSqlType.any, + requiredDuringInsert: false, + $customConstraints: ''); + @override + List 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 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 get $primaryKey => const {}; + @override + GeopolyTestData map(Map 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 { + final GeopolyPolygon? shape; + final DriftAny? a; + const GeopolyTestData({this.shape, this.a}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || shape != null) { + map['_shape'] = + Variable(shape, const GeopolyPolygonType()); + } + if (!nullToAbsent || a != null) { + map['a'] = Variable(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 json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GeopolyTestData( + shape: serializer.fromJson(json['_shape']), + a: serializer.fromJson(json['a']), + ); + } + factory GeopolyTestData.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + GeopolyTestData.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + '_shape': serializer.toJson(shape), + 'a': serializer.toJson(a), + }; + } + + GeopolyTestData copyWith( + {Value shape = const Value.absent(), + Value 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 { + final Value shape; + final Value a; + final Value 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 custom({ + Expression? shape, + Expression? a, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shape != null) '_shape': shape, + if (a != null) 'a': a, + if (rowid != null) 'rowid': rowid, + }); + } + + GeopolyTestCompanion copyWith( + {Value? shape, Value? a, Value? rowid}) { + return GeopolyTestCompanion( + shape: shape ?? this.shape, + a: a ?? this.a, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shape.present) { + map['_shape'] = + Variable(shape.value, const GeopolyPolygonType()); + } + if (a.present) { + map['a'] = Variable(a.value); + } + if (rowid.present) { + map['rowid'] = Variable(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 area(int var1) { + return customSelect( + 'SELECT geopoly_area(_shape) AS _c0 FROM geopoly_test WHERE "rowid" = ?1', + variables: [ + Variable(var1) + ], + readsFrom: { + geopolyTest, + }).map((QueryRow row) => row.readNullable('_c0')); + } + + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [geopolyTest]; +} diff --git a/drift/test/test_utils/test_utils.mocks.dart b/drift/test/test_utils/test_utils.mocks.dart index 03d207d3..0d052c05 100644 --- a/drift/test/test_utils/test_utils.mocks.dart +++ b/drift/test/test_utils/test_utils.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.3 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in drift/test/test_utils/test_utils.dart. // Do not manually edit this file. diff --git a/drift_dev/test/backends/build/build_integration_test.dart b/drift_dev/test/backends/build/build_integration_test.dart index d81f397f..e1828132 100644 --- a/drift_dev/test/backends/build/build_integration_test.dart +++ b/drift_dev/test/backends/build/build_integration_test.dart @@ -583,6 +583,8 @@ class MyDatabase { return everyElement( anyOf( isA().having((e) => e.extension, 'extension', '.json'), + // Allow reading SDK or other package assets to set up the analyzer. + isA().having((e) => e.package, 'package', isNot('a')), other, ), );