diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ae20e..48d2ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.2.0 + +- Supporting Unix socket connections. (Thanks to [grillbiff](https://github.com/grillbiff), + [#124](https://github.com/stablekernel/postgresql-dart/pull/124)) +- Preparation for custom type converters. + ## 2.1.1 - Fix `RuneIterator.current` use, which no longer returns `null` in 2.8 SDK. diff --git a/README.md b/README.md index f717a96..82aae83 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This driver uses the more efficient and secure extended query format of the Post Create `PostgreSQLConnection`s and `open` them: ```dart -var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); +var connection = PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await connection.open(); ``` @@ -47,18 +47,13 @@ Execute queries in a transaction: ```dart await connection.transaction((ctx) async { var result = await ctx.query("SELECT id FROM table"); - await ctx.query("INSERT INTO table (id) VALUES (@a:int4)", { + await ctx.query("INSERT INTO table (id) VALUES (@a:int4)", substitutionValues: { "a" : result.last[0] + 1 }); }); ``` -See the API documentation: https://www.dartdocs.org/documentation/postgres/latest. - -## Development branch - -The package's upcoming 2.0 version is being developed in the -[`dev`](https://github.com/stablekernel/postgresql-dart/tree/dev) branch. +See the API documentation: https://pub.dev/documentation/postgres/latest/ ## Features and bugs diff --git a/lib/postgres.dart b/lib/postgres.dart index 01c9235..1605a0a 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -1,6 +1,12 @@ library postgres; +import 'package:postgres/src/types.dart'; + +export 'package:dart_jts/dart_jts.dart' show WKBReader,Geometry; export 'src/connection.dart'; export 'src/execution_context.dart'; export 'src/substituter.dart'; export 'src/types.dart'; + + +Map typeMap = {}; diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index 89c75ce..5e21b05 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -199,6 +199,16 @@ class PostgresBinaryEncoder extends Converter { } return outBuffer; } + case PostgreSQLDataType.geometry: + { + if (value is Geometry) { + return castBytes( + utf8.encode( + value.toText(), + ), + ); ///TODO: Is this neccessary since I've added it to `PostgresTextEncoder` + } + } } throw PostgreSQLException('Unsupported datatype'); @@ -209,6 +219,7 @@ class PostgresBinaryDecoder extends Converter { const PostgresBinaryDecoder(this.typeCode); final int typeCode; + // final Map typeMap; @override dynamic convert(Uint8List value) { @@ -277,6 +288,17 @@ class PostgresBinaryDecoder extends Converter { return buf.toString(); } + case PostgreSQLDataType.geometry: + { + final wkbReader = + WKBReader(); // postgis geometries are stored as Well Known Binaries (https://postgis.net/docs/using_postgis_dbmanagement.html) + final geometry = wkbReader.read(value); + if (geometry is Geometry) { + return geometry; + } else { + throw PostgreSQLException('Error parsing geometry'); + } + } } // We'll try and decode this as a utf8 string and return that @@ -290,20 +312,22 @@ class PostgresBinaryDecoder extends Converter { } } - static final Map typeMap = { - 16: PostgreSQLDataType.boolean, - 17: PostgreSQLDataType.byteArray, - 19: PostgreSQLDataType.name, - 20: PostgreSQLDataType.bigInteger, - 21: PostgreSQLDataType.smallInteger, - 23: PostgreSQLDataType.integer, - 25: PostgreSQLDataType.text, - 700: PostgreSQLDataType.real, - 701: PostgreSQLDataType.double, - 1082: PostgreSQLDataType.date, - 1114: PostgreSQLDataType.timestampWithoutTimezone, - 1184: PostgreSQLDataType.timestampWithTimezone, - 2950: PostgreSQLDataType.uuid, - 3802: PostgreSQLDataType.json, - }; + // static final Map typeMap = { + // 16: PostgreSQLDataType.boolean, + // 17: PostgreSQLDataType.byteArray, + // 19: PostgreSQLDataType.name, + // 20: PostgreSQLDataType.bigInteger, + // 21: PostgreSQLDataType.smallInteger, + // 23: PostgreSQLDataType.integer, + // 25: PostgreSQLDataType.text, + // 700: PostgreSQLDataType.real, + // 701: PostgreSQLDataType.double, + // 1082: PostgreSQLDataType.date, + // 1114: PostgreSQLDataType.timestampWithoutTimezone, + // 1184: PostgreSQLDataType.timestampWithTimezone, + // 2950: PostgreSQLDataType.uuid, + // 3802: PostgreSQLDataType.json, + // 46315: PostgreSQLDataType.geometry, /// TODO: Oid Changes on different databases after running `CREATE EXTENSION postgis` + // 46971: PostgreSQLDataType.geometry /// `SELECT oid, typarray FROM pg_type WHERE typname in ('geometry','geography')`; + // }; } diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 841e012..3f5b4a9 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:buffer/buffer.dart'; +import 'package:postgres/postgres.dart'; import 'client_messages.dart'; import 'execution_context.dart'; @@ -38,13 +39,18 @@ class PostgreSQLConnection extends Object /// [queryTimeoutInSeconds] refers to the default timeout for [PostgreSQLExecutionContext]'s execute and query methods. /// [timeZone] is the timezone the connection is in. Defaults to 'UTC'. /// [useSSL] when true, uses a secure socket when connecting to a PostgreSQL database. - PostgreSQLConnection(this.host, this.port, this.databaseName, - {this.username, - this.password, - this.timeoutInSeconds = 30, - this.queryTimeoutInSeconds = 30, - this.timeZone = 'UTC', - this.useSSL = false}) { + PostgreSQLConnection( + this.host, + this.port, + this.databaseName, { + this.username, + this.password, + this.timeoutInSeconds = 30, + this.queryTimeoutInSeconds = 30, + this.timeZone = 'UTC', + this.useSSL = false, + this.isUnixSocket = false, + }) { _connectionState = _PostgreSQLConnectionStateClosed(); _connectionState.connection = this; } @@ -82,6 +88,9 @@ class PostgreSQLConnection extends Object /// The processID of this backend. int get processID => _processID; + /// If true, connection is made via unix socket. + final bool isUnixSocket; + /// Stream of notification from the database. /// /// Listen to this [Stream] to receive events from PostgreSQL NOTIFY commands. @@ -112,6 +121,8 @@ class PostgreSQLConnection extends Object int _secretKey; List _salt; + final Map _extraDataTypes = {}; + bool _hasConnectedPreviously = false; _PostgreSQLConnectionState _connectionState; @@ -129,7 +140,7 @@ class PostgreSQLConnection extends Object /// /// Connections may not be reopened after they are closed or opened more than once. If a connection has already been /// opened and this method is called, an exception will be thrown. - Future open() async { + Future open({bool enablePostGISSupport = false}) async { if (_hasConnectedPreviously) { throw PostgreSQLException( 'Attempting to reopen a closed connection. Create a instance instead.'); @@ -137,8 +148,14 @@ class PostgreSQLConnection extends Object try { _hasConnectedPreviously = true; - _socket = await Socket.connect(host, port) - .timeout(Duration(seconds: timeoutInSeconds)); + if (isUnixSocket) { + _socket = await Socket.connect( + InternetAddress(host, type: InternetAddressType.unix), port) + .timeout(Duration(seconds: timeoutInSeconds)); + } else { + _socket = await Socket.connect(host, port) + .timeout(Duration(seconds: timeoutInSeconds)); + } _framer = MessageFramer(); if (useSSL) { @@ -163,6 +180,66 @@ class PostgreSQLConnection extends Object rethrow; } + + if(enablePostGISSupport) { + //CREATE EXTENSION postgis; + try { + await _connection.execute('CREATE EXTENSION IF NOT EXISTS postgis'); + } catch (e,st) { + await _close(e, st); + rethrow; + } + } + + await _updateIDS(); + } + + Future updateIDS() => _updateIDS(); + + Future _updateIDS() async { + typeMap = typeMap.isNotEmpty ? typeMap : { + 16: PostgreSQLDataType.boolean, + 17: PostgreSQLDataType.byteArray, + 19: PostgreSQLDataType.name, + 20: PostgreSQLDataType.bigInteger, + 21: PostgreSQLDataType.smallInteger, + 23: PostgreSQLDataType.integer, + 25: PostgreSQLDataType.text, + 700: PostgreSQLDataType.real, + 701: PostgreSQLDataType.double, + 1082: PostgreSQLDataType.date, + 1114: PostgreSQLDataType.timestampWithoutTimezone, + 1184: PostgreSQLDataType.timestampWithTimezone, + 2950: PostgreSQLDataType.uuid, + 3802: PostgreSQLDataType.json, + }; + + // if (enablePostGISSupport) { + + // fetch oids from database (dynamic values) + final dataTypes = await _connection._query( + ''' + select oid::int,typname from pg_type where typname in ('text','int2','int4','int8','float4','float8','bool','date','bytea', 'timestamp','timestamptz','jsonb','name','uuid','geometry', 'geography'); + ''', + ); + + final mapped = dataTypes.map((row) { + final oid = row[0] as int; + final typname = row[1] as String; + return MapEntry(typname, oid); + }); + _extraDataTypes.addEntries(mapped); + // } + + typeMap = _extraDataTypes.map((key, value) { + // add boolean since it's not called bool on pg_types + if (key == 'bool') { + return MapEntry( + value, PostgreSQLFormatIdentifier.typeStringToCodeMap['boolean']); + } + return MapEntry( + value, PostgreSQLFormatIdentifier.typeStringToCodeMap[key]); + }); } /// Closes a connection. @@ -425,7 +502,7 @@ abstract class _PostgreSQLExecutionContextMixin } final query = Query>>( - fmtString, substitutionValues, _connection, _transaction); + fmtString, substitutionValues, _connection, _transaction, typeMap); if (allowReuse) { query.statementIdentifier = _connection._cache.identifierForQuery(query); } @@ -470,7 +547,7 @@ abstract class _PostgreSQLExecutionContextMixin } final query = Query( - fmtString, substitutionValues, _connection, _transaction, + fmtString, substitutionValues, _connection, _transaction, typeMap, onlyReturnAffectedRowCount: true); return _enqueue(query, timeoutInSeconds: timeoutInSeconds); diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 36198db..d0e22ff 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -6,6 +6,12 @@ import 'substituter.dart'; import 'types.dart'; abstract class PostgreSQLExecutionContext { + + // final Map typeMap; + + // PostgreSQLExecutionContext(this.typeMap); + + /// Returns this context queue size int get queueSize; diff --git a/lib/src/query.dart b/lib/src/query.dart index 43e5c58..ba5e86a 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -18,11 +18,14 @@ class Query { this.statement, this.substitutionValues, this.connection, - this.transaction, { + this.transaction, + this.typeMap, + { this.onlyReturnAffectedRowCount = false, }); final bool onlyReturnAffectedRowCount; + final Map typeMap; String statementIdentifier; @@ -121,8 +124,7 @@ class Query { return true; } - final actualType = PostgresBinaryDecoder - .typeMap[actualParameterTypeCodeIterator.current]; + final actualType = typeMap[actualParameterTypeCodeIterator.current]; return actualType == specifiedType; }).any((v) => v == false); @@ -209,8 +211,9 @@ class ParameterValue { factory ParameterValue.text(dynamic value) { Uint8List bytes; if (value != null) { - final converter = PostgresTextEncoder(false); - bytes = castBytes(utf8.encode(converter.convert(value))); + final converter = PostgresTextEncoder(); + bytes = castBytes( + utf8.encode(converter.convert(value, escapeStrings: false))); } final length = bytes?.length ?? 0; return ParameterValue._(false, bytes, length); @@ -315,7 +318,9 @@ class PostgreSQLFormatIdentifier { 'jsonb': PostgreSQLDataType.json, 'bytea': PostgreSQLDataType.byteArray, 'name': PostgreSQLDataType.name, - 'uuid': PostgreSQLDataType.uuid + 'uuid': PostgreSQLDataType.uuid, + 'geometry': PostgreSQLDataType.geometry, + 'geography': PostgreSQLDataType.geometry }; factory PostgreSQLFormatIdentifier(String t) { diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index 4ee1f9a..70677ad 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:buffer/buffer.dart'; +import 'package:postgres/postgres.dart'; import 'connection.dart'; import 'query.dart'; diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index e1b1a7c..b2cc801 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -47,6 +47,8 @@ class PostgreSQLFormat { return 'name'; case PostgreSQLDataType.uuid: return 'uuid'; + case PostgreSQLDataType.geometry: + return 'geometry'; } return null; @@ -54,7 +56,7 @@ class PostgreSQLFormat { static String substitute(String fmtString, Map values, {SQLReplaceIdentifierFunction replace}) { - final converter = PostgresTextEncoder(true); + final converter = PostgresTextEncoder(); values ??= {}; replace ??= (spec, index) => converter.convert(values[spec.name]); diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index 1cecca0..284fc1b 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -2,45 +2,44 @@ import 'dart:convert'; import 'package:postgres/postgres.dart'; -class PostgresTextEncoder extends Converter { - const PostgresTextEncoder(this._escapeStrings); - - final bool _escapeStrings; - - @override - String convert(dynamic value) { +class PostgresTextEncoder { + String convert(dynamic value, {bool escapeStrings = true}) { if (value == null) { return 'null'; } if (value is int) { - return encodeNumber(value); + return _encodeNumber(value); } if (value is double) { - return encodeDouble(value); + return _encodeDouble(value); } if (value is String) { - return encodeString(value, _escapeStrings); + return _encodeString(value, escapeStrings); } if (value is DateTime) { - return encodeDateTime(value, isDateOnly: false); + return _encodeDateTime(value, isDateOnly: false); } if (value is bool) { - return encodeBoolean(value); + return _encodeBoolean(value); } if (value is Map) { - return encodeJSON(value); + return _encodeJSON(value); + } + + if (value is Geometry) { + return value.toText(); } throw PostgreSQLException("Could not infer type of value '$value'."); } - String encodeString(String text, bool escapeStrings) { + String _encodeString(String text, bool escapeStrings) { if (!escapeStrings) { return text; } @@ -85,7 +84,7 @@ class PostgresTextEncoder extends Converter { return buf.toString(); } - String encodeNumber(num value) { + String _encodeNumber(num value) { if (value.isNaN) { return "'nan'"; } @@ -97,7 +96,7 @@ class PostgresTextEncoder extends Converter { return value.toInt().toString(); } - String encodeDouble(double value) { + String _encodeDouble(double value) { if (value.isNaN) { return "'nan'"; } @@ -109,11 +108,11 @@ class PostgresTextEncoder extends Converter { return value.toString(); } - String encodeBoolean(bool value) { + String _encodeBoolean(bool value) { return value ? 'TRUE' : 'FALSE'; } - String encodeDateTime(DateTime value, {bool isDateOnly}) { + String _encodeDateTime(DateTime value, {bool isDateOnly}) { var string = value.toIso8601String(); if (isDateOnly) { @@ -147,7 +146,7 @@ class PostgresTextEncoder extends Converter { return "'$string'"; } - String encodeJSON(dynamic value) { + String _encodeJSON(dynamic value) { if (value == null) { return 'null'; } diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index 67ea2b1..b57f199 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -8,7 +8,7 @@ class _TransactionProxy extends Object implements PostgreSQLExecutionContext { _TransactionProxy( this._connection, this.executionBlock, this.commitTimeoutInSeconds) { - _beginQuery = Query('BEGIN', {}, _connection, this, + _beginQuery = Query('BEGIN', {}, _connection, this,typeMap, onlyReturnAffectedRowCount: true); _beginQuery.future.then(startTransaction).catchError((err, StackTrace st) { @@ -34,6 +34,7 @@ class _TransactionProxy extends Object bool _hasFailed = false; bool _hasRolledBack = false; + @override void cancelTransaction({String reason}) { throw _TransactionRollbackException(reason); @@ -87,7 +88,7 @@ class _TransactionProxy extends Object 'that prevented this query from executing.'); _queue.cancel(err); - final rollback = Query('ROLLBACK', {}, _connection, _transaction, + final rollback = Query('ROLLBACK', {}, _connection, _transaction,typeMap, onlyReturnAffectedRowCount: true); _queue.addEvenIfCancelled(rollback); diff --git a/lib/src/types.dart b/lib/src/types.dart index ee76281..9cfb48f 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -64,5 +64,9 @@ enum PostgreSQLDataType { /// /// Must contain 32 hexadecimal characters. May contain any number of '-' characters. /// When returned from database, format will be xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. - uuid + uuid, + + /// Must be a [List] of [int] currently in Ewkb(Extended Well-Known Binary) format + /// (https://postgis.net/docs/using_postgis_dbmanagement.html#OpenGISWKBWKT) + geometry } diff --git a/pubspec.yaml b/pubspec.yaml index a09c233..9de028d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,14 +1,15 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 2.1.1 +version: 2.2.0-dev homepage: https://github.com/stablekernel/postgresql-dart environment: - sdk: ">=2.2.0 <3.0.0" + sdk: ">=2.8.0 <3.0.0" dependencies: buffer: ^1.0.6 crypto: ^2.0.0 + dart_jts: ^0.0.5+3 dev_dependencies: pedantic: ^1.0.0 diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 877f1cf..f5310bf 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -232,8 +232,9 @@ void main() { }); group('Text encoders', () { + final encoder = PostgresTextEncoder(); + test('Escape strings', () { - final encoder = PostgresTextEncoder(true); // ' b o b ' expect( utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); @@ -294,9 +295,8 @@ void main() { DateTime(12345, DateTime.february, 3, 4, 5, 6, 0) }; - final encoder = PostgresTextEncoder(false); pairs.forEach((k, v) { - expect(encoder.convert(v), "'$k'"); + expect(encoder.convert(v, escapeStrings: false), "'$k'"); }); }); @@ -311,37 +311,29 @@ void main() { '0.0': 0.0 }; - final encoder = PostgresTextEncoder(false); pairs.forEach((k, v) { - expect(encoder.convert(v), '$k'); + expect(encoder.convert(v, escapeStrings: false), '$k'); }); }); test('Encode Int', () { - final encoder = PostgresTextEncoder(false); - expect(encoder.convert(1), '1'); expect(encoder.convert(1234324323), '1234324323'); expect(encoder.convert(-1234324323), '-1234324323'); }); test('Encode Bool', () { - final encoder = PostgresTextEncoder(false); - expect(encoder.convert(true), 'TRUE'); expect(encoder.convert(false), 'FALSE'); }); test('Encode JSONB', () { - final encoder = PostgresTextEncoder(false); - expect(encoder.convert({'a': 'b'}), '{"a":"b"}'); expect(encoder.convert({'a': true}), '{"a":true}'); expect(encoder.convert({'b': false}), '{"b":false}'); }); test('Attempt to infer unknown type throws exception', () { - final encoder = PostgresTextEncoder(false); try { encoder.convert([]); fail('unreachable'); @@ -413,7 +405,7 @@ Future expectInverse(dynamic value, PostgreSQLDataType dataType) async { dataType = PostgreSQLDataType.bigInteger; } int code; - PostgresBinaryDecoder.typeMap.forEach((key, type) { + typeMap.forEach((key, type) { if (type == dataType) { code = key; } diff --git a/test/geometry_test.dart b/test/geometry_test.dart new file mode 100644 index 0000000..2f48812 --- /dev/null +++ b/test/geometry_test.dart @@ -0,0 +1,423 @@ +import 'package:postgres/postgres.dart'; +import 'package:test/test.dart'; +import 'package:dart_jts/dart_jts.dart'; + +const String WKT_POINT = 'POINT ( 10 10)'; + +const String WKT_LINESTRING = 'LINESTRING (10 10, 20 20, 30 40)'; + +const String WKT_LINEARRING = 'LINEARRING (10 10, 20 20, 30 40, 10 10)'; + +const String WKT_POLY = 'POLYGON ((50 50, 50 150, 150 150, 150 50, 50 50))'; + +const String WKT_MULTIPOINT = 'MULTIPOINT ((10 10), (20 20))'; + +const String WKT_MULTILINESTRING = + 'MULTILINESTRING ((10 10, 20 20), (15 15, 30 15))'; + +const String WKT_MULTIPOLYGON = + 'MULTIPOLYGON (((10 10, 10 20, 20 20, 20 15, 10 10)), ((60 60, 70 70, 80 60, 60 60)))'; + +const String WKT_GC = + 'GEOMETRYCOLLECTION (POLYGON ((100 200, 200 200, 200 100, 100 100, 100 200)), LINESTRING (150 250, 250 250))'; + +const String multiInsert = ''' + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POINT(0 0)')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POINT(-2 2)')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTIPOINT(2 1,1 2)')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;LINESTRING(0 0,1 1,1 2)')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTILINESTRING((1 0,0 1,3 2),(3 2,5 4))')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTIPOLYGON(((1 1,3 1,3 3,1 3,1 1),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))')); + INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1, 2 1, 2 2, 1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))')); + '''; + +/// Before running this tests, RUN `SELECT oid, typname FROM pg_type WHERE typname in ('geometry','geography');` AND change the id of `typeMap` in `PostgresBinaryDecoder` to the returned oid. +/// after running `CREATE EXTENSION postgis` in the database +/// WorkAround for this needed + +void main() { + PostgreSQLConnection connection; + + final geomFactory = GeometryFactory.withCoordinateSequenceFactory( + PackedCoordinateSequenceFactory.withType( + PackedCoordinateSequenceFactory.DOUBLE, + ), + ); + + final rdr = WKTReader.withFactory(geomFactory); + + setUp(() async { + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); + await connection.open(enablePostGISSupport: true); + + await connection.execute(''' + DROP TABLE IF EXISTS test; + CREATE EXTENSION IF NOT EXISTS postgis; + CREATE TABLE IF NOT EXISTS test(gid serial PRIMARY KEY, geom geometry); + '''); + }); + + tearDown(() async { + await connection?.close(); + }); + + test('GeometryCollection Equality', () { + + final WKT_GC = + 'GEOMETRYCOLLECTION (POLYGON ((100 200, 200 200, 200 100, 100 100, 100 200)), LINESTRING (150 250, 250 250))'; + + final geometryCollection = rdr.read(WKT_GC); + final geometryCollection2 = rdr.read(WKT_GC); + + expect(geometryCollection.equals(geometryCollection2), true); + + }); + + test( + 'Inserting geometries in plain dart objects should be inserted using geometry.toText()', + () async { + final point = rdr.read(WKT_POINT); + final lineString = rdr.read(WKT_LINESTRING); + final polygon = rdr.read(WKT_POLY); + final multiPoint = rdr.read(WKT_MULTIPOINT); + final multiLineString = rdr.read(WKT_MULTILINESTRING); + final multiPolygon = rdr.read(WKT_MULTIPOLYGON); + final geometryCollection = rdr.read(WKT_GC); + + final result = await connection.query( + 'INSERT into test(geom) VALUES (@point),(@lineString),(@polygon),(@multiPoint),(@multiPolygon),(@multiLineString),(@geometryCollection) returning geom,geom,geom,geom,geom,geom,geom', + substitutionValues: { + 'point': point, + 'lineString': lineString, + 'polygon': polygon, + 'multiPolygon': multiPolygon, + 'multiLineString': multiLineString, + 'multiPoint': multiPoint, + 'geometryCollection' : geometryCollection + }, + ); + + expect(point.equals(result[0][0] as Point), true); + expect(lineString.equals(result[1][0] as LineString), true); + expect(polygon.equals(result[2][0] as Polygon), true); + expect(multiPoint.equals(result[3][0] as MultiPoint), true); + expect(multiPolygon.equals(result[4][0] as MultiPolygon), true); + expect(multiLineString.equals(result[5][0] as MultiLineString), true); + // expect(geometryCollection.equals(result[6][0] as GeometryCollection), true); /// TODO: Issue with checking equality on GeometryCollection. (https://github.com/moovida/dart_jts/issues/2#issuecomment-653381031) + + }); + + test('Can store point and read point as dart_jts.Point', () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@point) returning geom', + substitutionValues: {'point': WKT_POINT}, + ); + final geom = result[0][0] as Point; + // print(geom.toString()); + + expect(geom.equalsExactGeom(rdr.read(WKT_POINT)), true); + }); + + test('Can store linestring and read linestring as dart_jts.LineString', + () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@linestring) returning geom', + substitutionValues: {'linestring': WKT_LINESTRING}, + ); + + final geom = result[0][0] as LineString; + // print(geom.toString()); + final lineString = rdr.read(WKT_LINESTRING); + + expect(geom.toString(), lineString.toString()); + expect(geom.SRID, lineString.SRID); + expect(geom.envelope, lineString.envelope); + expect(geom.equals(lineString), true); + + expect(geom.toText(), lineString.toText()); + }); + + // test('Can store linearring and read linearring as dart_jts.LinearRing', + // () async { + // final result = await connection.query( + // 'INSERT into test(geom) VALUES (@linearRing) returning ST_IsValid(geom)', + // substitutionValues: {'linearRing': rdr.read(WKT_LINEARRING).toText()}, + // ); + // // final geom = result[0][0] as LinearRing; + // // final linearRing = rdr.read(WKT_LINEARRING); + + // // expect(geom.equalsExactGeom(linearRing), true); + // // expect(geom.SRID, linearRing.SRID); + // // expect(geom.toText(), linearRing.toText()); + // expect(result[0][0], false); + // }); + + test('Can store polygon and read it as dart_jts.Polygon', () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@polygon) returning geom', + substitutionValues: {'polygon': WKT_POLY}, + ); + + final geom = result[0][0] as Polygon; + final poly = rdr.read(WKT_POLY); + + expect(geom.equalsExactGeom(poly), true); + expect(geom.SRID, poly.SRID); + expect(geom.toText(), poly.toText()); + }); + + test('Can store MultiPoint and read it as dart_jts.MultiPolygon', () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@multiPoint) returning geom', + substitutionValues: {'multiPoint': WKT_MULTIPOINT}, + ); + + final geom = result[0][0] as MultiPoint; + final multiPoint = rdr.read(WKT_MULTIPOINT); + + expect(geom.equalsExactGeom(multiPoint), true); + expect(geom.SRID, multiPoint.SRID); + expect(geom.toText(), multiPoint.toText()); + }); + + test('Can store MultiLineString well and read it as dart_jts.MultiLineString', + () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@multiLine),(@multiLine) returning geom,geom', + substitutionValues: {'multiLine': WKT_MULTILINESTRING}, + ); + + final geom = result[0][0] as MultiLineString; + final geom1 = result[0][1] as MultiLineString; + final multiLineString = rdr.read(WKT_MULTILINESTRING); + + expect(geom.equals(geom1), true); + + expect(geom.equalsExactGeom(multiLineString), true); + expect(geom.SRID, multiLineString.SRID); + expect(geom.toText(), multiLineString.toText()); + }); + + test('Can store multipolygon and read it as dart_jts.MultiPolgon', () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@multiPoly) returning geom', + substitutionValues: {'multiPoly': WKT_MULTIPOLYGON}, + ); + final geom = result[0][0] as MultiPolygon; + final actualGeom = rdr.read(WKT_MULTIPOLYGON); + + expect(actualGeom.equals(geom), true); + expect(geom.SRID, actualGeom.SRID); + expect(geom.toText(), actualGeom.toText()); + }); + + test('Can store GeometryCollection well', () async { + final result = await connection.query( + 'INSERT into test(geom) VALUES (@geomColl) returning geom', + substitutionValues: {'geomColl': WKT_GC}, + ); + + final geom = result[0][0] as GeometryCollection; + // final actualGeom = rdr.read(WKT_GC); //TODO: Issue with wkt reading GeometryCollections + + // expect(actualGeom.equals(geom), true); + // expect(geom.SRID, actualGeom.SRID); + expect(geom.toText(), WKT_GC); + }); + + test( + 'MultiInsert should return appropriate inserted geometries when read back', + () async { + final sql = ''' + INSERT INTO test(geom) values + (GeomFromEWKT('SRID=4326;POINT(0 0)')), + (GeomFromEWKT('SRID=4326;POINT(-2 2)')), + (GeomFromEWKT('SRID=4326;MULTIPOINT(2 1,1 2)')), + (GeomFromEWKT('SRID=4326;LINESTRING(0 0,1 1,1 2)')), + (GeomFromEWKT('SRID=4326;MULTILINESTRING((1 0,0 1,3 2),(3 2,5 4))')), + (GeomFromEWKT('SRID=4326;POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))')), + (GeomFromEWKT('SRID=4326;MULTIPOLYGON(((1 1,3 1,3 3,1 3,1 1),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))')), + (GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1, 2 1, 2 2, 1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))')) + RETURNING geom,geom,geom,geom,geom,geom,geom,geom + '''; + final results = await connection.query(sql); + final point = results[0][0] as Point; + final point2 = results[1][0] as Point; + final multiPoint = results[2][0] as MultiPoint; + final lineString = results[3][0] as LineString; + final multiLineString = results[4][0] as MultiLineString; + final polygon = results[5][0] as Polygon; + final multiPolygon = results[6][0] as MultiPolygon; + final geomCollection = results[7][0] as GeometryCollection; + + expect(point.SRID, 4326); + expect(point.coordinates.getX(0), 0); + expect(point.coordinates.getY(0), 0); + + expect(point2.SRID, 4326); + expect(point2.coordinates.getX(0), -2); + expect(point2.coordinates.getY(0), 2); + + expect(multiPoint.getCoordinates().first, Coordinate(2, 1)); + expect(multiPoint.getCoordinates().elementAt(1), Coordinate(1, 2)); + + expect(lineString.getCoordinates().length, 3); + expect(lineString.getCoordinates().elementAt(0), Coordinate(0, 0)); + expect(lineString.getCoordinates().elementAt(1), Coordinate(1, 1)); + expect(lineString.getCoordinates().elementAt(2), Coordinate(1, 2)); + expect(lineString.SRID, 4326); + + expect(multiLineString.SRID, 4326); + expect(multiLineString.getNumGeometries(), 2); + expect( + multiLineString.getGeometryN(0).equals( + geomFactory.createLineString( + [ + Coordinate(1, 0), + Coordinate(0, 1), + Coordinate(3, 2), + ], + ), + ), + true, + ); + expect( + multiLineString.getGeometryN(1).equals( + geomFactory.createLineString( + [ + Coordinate(3, 2), + Coordinate(5, 4), + ], + ), + ), + true, + ); + + expect(polygon.SRID, 4326); + expect(polygon.getNumInteriorRing(), 1); + expect( + polygon.getInteriorRingN(0).equals( + geomFactory.createLinearRing( + [ + Coordinate(1, 1), + Coordinate(2, 1), + Coordinate(2, 2), + Coordinate(1, 2), + Coordinate(1, 1), + ], + ), + ), + true, + ); + + expect( + polygon.getExteriorRing().equals( + geomFactory.createLinearRing( + [ + Coordinate(0, 0), + Coordinate(4, 0), + Coordinate(4, 4), + Coordinate(0, 4), + Coordinate(0, 0) + ], + ), + ), + true); + + expect(multiPolygon.getNumGeometries(), 2); + expect(multiPolygon.SRID, 4326); + final polygon1 = multiPolygon.getGeometryN(0) as Polygon; + final polygon2 = multiPolygon.getGeometryN(1) as Polygon; + + expect(polygon1.getGeometryType(), 'Polygon'); + expect(polygon2.getGeometryType(), 'Polygon'); + + expect(polygon1.getNumInteriorRing(), 1); + expect( + polygon1.getInteriorRingN(0).equals( + geomFactory.createLinearRing( + [ + //1 1,2 1,2 2,1 2,1 1 + Coordinate(1, 1), + Coordinate(2, 1), + Coordinate(2, 2), + Coordinate(1, 2), + Coordinate(1, 1) + ], + ), + ), + true, + ); + + expect( + polygon1.getExteriorRing().equals( + geomFactory.createLinearRing( + [ + //1 1,3 1,3 3,1 3,1 1 + Coordinate(1, 1), + Coordinate(3, 1), + Coordinate(3, 3), + Coordinate(1, 3), + Coordinate(1, 1) + ], + ), + ), + true, + ); + + expect( + polygon2.getExteriorRing().equals( + geomFactory.createLinearRing( + [ + //-1 -1,-1 -2,-2 -2,-2 -1,-1 -1 + Coordinate(-1, -1), + Coordinate(-1, -2), + Coordinate(-2, -2), + Coordinate(-2, -1), + Coordinate(-1, -1) + ], + ), + ), + true, + ); + + expect(geomCollection.SRID, 4326); + expect(geomCollection.getNumGeometries(), 3); + + final polygonInGC = geomCollection.getGeometryN(0) as Polygon; + final pointInGC = geomCollection.getGeometryN(1) as Point; + final lineStringInGC = geomCollection.getGeometryN(2) as LineString; + + expect( + polygonInGC.getExteriorRing().equals( + geomFactory.createLinearRing( + [ + Coordinate(1, 1), + Coordinate(2, 1), + Coordinate(2, 2), + Coordinate(1, 2), + Coordinate(1, 1) + ], + ), + ), + true, + ); + + expect(pointInGC.getCoordinate().getX(), 2); + expect(pointInGC.getCoordinate().getY(), 3); + + expect( + lineStringInGC.equals( + geomFactory.createLineString( + [ + Coordinate(2, 3), + Coordinate(3, 4), + ], + ), + ), + true, + ); + }); +}