diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart index 26b1211d..0fec915e 100644 --- a/drift/lib/src/runtime/types/mapping.dart +++ b/drift/lib/src/runtime/types/mapping.dart @@ -81,6 +81,15 @@ final class SqlTypes { return dartValue.rawSqlValue; } + if (dartValue is GeopolyPolygon) { + switch (dartValue) { + case _StringGeopolyPolygon(:final value): + return value; + case _BlobGeopolyPolygon(:final value): + return value; + } + } + return dartValue; } @@ -534,3 +543,25 @@ final class _ByDialectType implements DialectAwareSqlType { return _selectType(context.typeMapping).sqlTypeName(context); } } + +/// https://www.sqlite.org/geopoly.html +/// In Geopoly, a polygon can be text or a blob +sealed class GeopolyPolygon { + const GeopolyPolygon._(); + + const factory GeopolyPolygon.text(String value) = _StringGeopolyPolygon; + + const factory GeopolyPolygon.blob(Uint8List value) = _BlobGeopolyPolygon; +} + +final class _StringGeopolyPolygon extends GeopolyPolygon { + final String value; + + const _StringGeopolyPolygon(this.value) : super._(); +} + +final class _BlobGeopolyPolygon extends GeopolyPolygon { + final Uint8List value; + + const _BlobGeopolyPolygon(this.value) : super._(); +} diff --git a/drift_dev/lib/src/analysis/driver/driver.dart b/drift_dev/lib/src/analysis/driver/driver.dart index 0be3d1ba..c9b0fdd8 100644 --- a/drift_dev/lib/src/analysis/driver/driver.dart +++ b/drift_dev/lib/src/analysis/driver/driver.dart @@ -83,6 +83,8 @@ 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, ), diff --git a/drift_dev/lib/src/analysis/options.dart b/drift_dev/lib/src/analysis/options.dart index fe298795..0c484920 100644 --- a/drift_dev/lib/src/analysis/options.dart +++ b/drift_dev/lib/src/analysis/options.dart @@ -400,6 +400,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. diff --git a/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart b/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart index 50a1d651..3b0e8bb8 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart @@ -76,9 +76,12 @@ 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)), + ColumnGeopolyPolygonType() => type.addHint(const IsGeopolyPolygon()), + }; + if (column.typeConverter case AppliedTypeConverter c) { type = type.addHint(TypeConverterHint(c)); } @@ -147,6 +150,10 @@ class TypeMapping { return ColumnType.custom(customHint.type); } + if (type.hint() != null) { + return const ColumnType.geopolyPolygon(); + } + return ColumnType.drift(_toDefaultType(type)); } } diff --git a/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart b/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart index 0a64ab0f..8d2f4821 100644 --- a/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart +++ b/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart @@ -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; } diff --git a/drift_dev/lib/src/analysis/results/dart.dart b/drift_dev/lib/src/analysis/results/dart.dart index 5171595d..641d70f2 100644 --- a/drift_dev/lib/src/analysis/results/dart.dart +++ b/drift_dev/lib/src/analysis/results/dart.dart @@ -131,18 +131,21 @@ 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); + case ColumnGeopolyPolygonType(:final dartType): + addTopLevel(dartType); + } if (hasType.nullable) addText('?'); } } diff --git a/drift_dev/lib/src/analysis/results/query.dart b/drift_dev/lib/src/analysis/results/query.dart index 014c18ba..db28749d 100644 --- a/drift_dev/lib/src/analysis/results/query.dart +++ b/drift_dev/lib/src/analysis/results/query.dart @@ -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 get scalarColumns => columns.whereType(); + Iterable 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() || ColumnGeopolyPolygonType() => 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; } } diff --git a/drift_dev/lib/src/analysis/results/types.dart b/drift_dev/lib/src/analysis/results/types.dart index 54dd42c8..5333221c 100644 --- a/drift_dev/lib/src/analysis/results/types.dart +++ b/drift_dev/lib/src/analysis/results/types.dart @@ -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 - @@ -45,13 +45,35 @@ class ColumnType { final DriftSqlType builtin; /// Details about the custom type, if one is present. - final CustomColumnType? custom; + // CustomColumnType? get custom => switch (this) { + // ColumnDriftType() || ColumnGeopolyPolygonType() => null, + // ColumnCustomType(:final custom) => custom, + // }; - bool get isCustom => custom != null; + const ColumnType._(this.builtin); - const ColumnType.drift(this.builtin) : custom = null; + const factory ColumnType.drift(DriftSqlType builtin) = ColumnDriftType; - ColumnType.custom(CustomColumnType this.custom) : builtin = DriftSqlType.any; + const factory ColumnType.custom(CustomColumnType custom) = ColumnCustomType; + + const factory ColumnType.geopolyPolygon() = ColumnGeopolyPolygonType; +} + +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); +} + +final class ColumnGeopolyPolygonType extends ColumnType { + const ColumnGeopolyPolygonType() : super._(DriftSqlType.any); + + DartTopLevelSymbol get dartType => + DartTopLevelSymbol('GeopolyPolygon', AnnotatedDartCode.drift); } extension OperationOnTypes on HasType { diff --git a/drift_dev/lib/src/analysis/serializer.dart b/drift_dev/lib/src/analysis/serializer.dart index b27ff839..746ac661 100644 --- a/drift_dev/lib/src/analysis/serializer.dart +++ b/drift_dev/lib/src/analysis/serializer.dart @@ -198,17 +198,20 @@ class ElementSerializer { } Map _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, - }; + switch (type) { + case ColumnGeopolyPolygonType(): + case ColumnDriftType(): + return { + 'builtin': type.builtin.name, + }; + case ColumnCustomType(:final custom): + return { + 'custom': { + 'dart': _serializeType(custom.dartType), + 'expression': custom.expression.toJson(), + } + }; + } } Map _serializeColumn(DriftColumn column) { diff --git a/drift_dev/lib/src/generated/analysis/options.g.dart b/drift_dev/lib/src/generated/analysis/options.g.dart index 4e86b7c9..efea74e7 100644 --- a/drift_dev/lib/src/generated/analysis/options.g.dart +++ b/drift_dev/lib/src/generated/analysis/options.g.dart @@ -174,6 +174,7 @@ const _$SqlModuleEnumMap = { SqlModule.math: 'math', SqlModule.rtree: 'rtree', SqlModule.spellfix1: 'spellfix1', + SqlModule.geopoly: 'geopoly', }; const _$CaseFromDartToSqlEnumMap = { diff --git a/drift_dev/lib/src/writer/queries/query_writer.dart b/drift_dev/lib/src/writer/queries/query_writer.dart index fb5be6a3..ddc9da12 100644 --- a/drift_dev/lib/src/writer/queries/query_writer.dart +++ b/drift_dev/lib/src/writer/queries/query_writer.dart @@ -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,15 @@ 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 ColumnGeopolyPolygonType(): + 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 +762,7 @@ class _ExpandedDeclarationWriter { class _ExpandedVariableWriter { final SqlQuery query; final TextEmitter _emitter; + StringBuffer get _buffer => _emitter.buffer; _ExpandedVariableWriter(this.query, this._emitter); diff --git a/drift_dev/lib/src/writer/tables/data_class_writer.dart b/drift_dev/lib/src/writer/tables/data_class_writer.dart index 289a8f06..25642540 100644 --- a/drift_dev/lib/src/writer/tables/data_class_writer.dart +++ b/drift_dev/lib/src/writer/tables/data_class_writer.dart @@ -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,13 @@ 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(): + case ColumnGeopolyPolygonType(): + sqlType = writer.drift(column.sqlType.builtin.toString()); + case ColumnCustomType(:final custom): + sqlType = writer.dartCode(custom.expression); } var loadType = '$databaseGetter.typeMapping.read($sqlType, $rawData)'; diff --git a/drift_dev/lib/src/writer/tables/table_writer.dart b/drift_dev/lib/src/writer/tables/table_writer.dart index 2ea4b6c4..f4c9baab 100644 --- a/drift_dev/lib/src/writer/tables/table_writer.dart +++ b/drift_dev/lib/src/writer/tables/table_writer.dart @@ -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; @@ -210,12 +211,13 @@ 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(): + case ColumnGeopolyPolygonType(): + additionalParams['type'] = + emitter.drift(column.sqlType.builtin.toString()); + case ColumnCustomType(:final custom): + additionalParams['type'] = emitter.dartCode(custom.expression); } if (isRequiredForInsert != null) { diff --git a/drift_dev/lib/src/writer/writer.dart b/drift_dev/lib/src/writer/writer.dart index a6b36dfc..6b9c1ba0 100644 --- a/drift_dev/lib/src/writer/writer.dart +++ b/drift_dev/lib/src/writer/writer.dart @@ -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(); } @@ -142,12 +144,13 @@ 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(): + case ColumnGeopolyPolygonType(): + sqlDartType = + AnnotatedDartCode([dartTypeNames[converter.sqlType.builtin]!]); + case ColumnCustomType(:final custom): + sqlDartType = AnnotatedDartCode.type(custom.dartType); } final className = converter.alsoAppliesToJsonConversion @@ -204,12 +207,13 @@ 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 ColumnGeopolyPolygonType(:final dartType): + b.addTopLevel(dartType); + case ColumnDriftType(): + b.addTopLevel(dartTypeNames[type.builtin]!); + case ColumnCustomType(:final custom): + b.addDartType(custom.dartType); } if (nullable) { @@ -240,12 +244,16 @@ 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(): + case ColumnGeopolyPolygonType(): + 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(')'); @@ -421,6 +429,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) { diff --git a/drift_dev/test/analysis/resolver/dart/column_test.dart b/drift_dev/test/analysis/resolver/dart/column_test.dart index 2e45c5ce..d21370c5 100644 --- a/drift_dev/test/analysis/resolver/dart/column_test.dart +++ b/drift_dev/test/analysis/resolver/dart/column_test.dart @@ -254,8 +254,14 @@ class TestTable extends Table { final column = table.columns.single; expect(column.sqlType.builtin, DriftSqlType.any); - expect(column.sqlType.custom?.dartType.toString(), 'List'); - expect(column.sqlType.custom?.expression.toString(), 'StringArrayType()'); + switch (column.sqlType) { + case ColumnDriftType(): + case ColumnGeopolyPolygonType(): + break; + case ColumnCustomType(:final custom): + expect(custom.dartType.toString(), 'List'); + expect(custom.expression.toString(), 'StringArrayType()'); + } }); group('customConstraint analysis', () { diff --git a/drift_dev/test/analysis/resolver/drift/table_test.dart b/drift_dev/test/analysis/resolver/drift/table_test.dart index cf81d1ae..09d251d8 100644 --- a/drift_dev/test/analysis/resolver/drift/table_test.dart +++ b/drift_dev/test/analysis/resolver/drift/table_test.dart @@ -1,6 +1,7 @@ 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'; @@ -286,8 +287,13 @@ class MyType implements CustomSqlType {} 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(): + case ColumnGeopolyPolygonType(): + fail('expect custom type'); + case ColumnCustomType(:final custom): + expect(custom.dartType.toString(), 'String'); + expect(custom.expression.toString(), 'MyType()'); + } }); } diff --git a/examples/migrations_example/lib/database.g.dart b/examples/migrations_example/lib/database.g.dart index f0656955..6e810776 100644 --- a/examples/migrations_example/lib/database.g.dart +++ b/examples/migrations_example/lib/database.g.dart @@ -282,7 +282,7 @@ class Groups extends Table with TableInfo { 'deleted', aliasedName, true, type: DriftSqlType.bool, requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', + $customConstraints: 'NULL DEFAULT FALSE', defaultValue: const CustomExpression('FALSE')); static const VerificationMeta _ownerMeta = const VerificationMeta('owner'); late final GeneratedColumn owner = GeneratedColumn( diff --git a/sqlparser/lib/sqlparser.dart b/sqlparser/lib/sqlparser.dart index 6fb44d39..b0fc9f44 100644 --- a/sqlparser/lib/sqlparser.dart +++ b/sqlparser/lib/sqlparser.dart @@ -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; diff --git a/sqlparser/lib/src/analysis/types/data.dart b/sqlparser/lib/src/analysis/types/data.dart index 33afe39c..a028f4ee 100644 --- a/sqlparser/lib/src/analysis/types/data.dart +++ b/sqlparser/lib/src/analysis/types/data.dart @@ -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 ==(dynamic 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, diff --git a/sqlparser/lib/src/engine/module/geopoly.dart b/sqlparser/lib/src/engine/module/geopoly.dart new file mode 100644 index 00000000..7bb6c80b --- /dev/null +++ b/sqlparser/lib/src/engine/module/geopoly.dart @@ -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 = [ + 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 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 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 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')); + } +} diff --git a/sqlparser/test/engine/module/geopoly_test.dart b/sqlparser/test/engine/module/geopoly_test.dart new file mode 100644 index 00000000..5eaa14de --- /dev/null +++ b/sqlparser/test/engine/module/geopoly_test.dart @@ -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)); + }); + }); +}