mirror of https://github.com/AMT-Cheif/drift.git
add geopoly support
This commit is contained in:
parent
364450d26a
commit
15001364c2
|
@ -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<T extends Object> implements DialectAwareSqlType<T> {
|
|||
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._();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<IsGeopolyPolygon>() != null) {
|
||||
return const ColumnType.geopolyPolygon();
|
||||
}
|
||||
|
||||
return ColumnType.drift(_toDefaultType(type));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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('?');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() || 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -198,17 +198,20 @@ 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,
|
||||
};
|
||||
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<String, Object?> _serializeColumn(DriftColumn column) {
|
||||
|
|
|
@ -174,6 +174,7 @@ const _$SqlModuleEnumMap = {
|
|||
SqlModule.math: 'math',
|
||||
SqlModule.rtree: 'rtree',
|
||||
SqlModule.spellfix1: 'spellfix1',
|
||||
SqlModule.geopoly: 'geopoly',
|
||||
};
|
||||
|
||||
const _$CaseFromDartToSqlEnumMap = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<String>');
|
||||
expect(column.sqlType.custom?.expression.toString(), 'StringArrayType()');
|
||||
switch (column.sqlType) {
|
||||
case ColumnDriftType():
|
||||
case ColumnGeopolyPolygonType():
|
||||
break;
|
||||
case ColumnCustomType(:final custom):
|
||||
expect(custom.dartType.toString(), 'List<String>');
|
||||
expect(custom.expression.toString(), 'StringArrayType()');
|
||||
}
|
||||
});
|
||||
|
||||
group('customConstraint analysis', () {
|
||||
|
|
|
@ -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<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():
|
||||
case ColumnGeopolyPolygonType():
|
||||
fail('expect custom type');
|
||||
case ColumnCustomType(:final custom):
|
||||
expect(custom.dartType.toString(), 'String');
|
||||
expect(custom.expression.toString(), 'MyType()');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -282,7 +282,7 @@ class Groups extends Table with TableInfo<Groups, Group> {
|
|||
'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<int> owner = GeneratedColumn<int>(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue