From f420e91873ca05f2f4543d7302b2743c3ff226b4 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 21 Nov 2016 09:40:17 -0500 Subject: [PATCH 01/73] Bump for docs --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fd22e..68a20cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.2 + +- Bump for documentation + ## 0.9.1 - Added transactions: PostgreSQLConnection.transaction and PostgreSQLConnection.cancelTransaction. diff --git a/pubspec.yaml b/pubspec.yaml index ad3e3a4..1896446 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: A library to connect and query PostgreSQL databases. -version: 0.9.1 +version: 0.9.2 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: From 2973f13407b452482132246fed259b06e337a933 Mon Sep 17 00:00:00 2001 From: Alex Nachlas Date: Wed, 30 Nov 2016 12:43:40 -0500 Subject: [PATCH 02/73] Use UTF8.encode and UTF8.decode (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use UTF8.encode and UTF8.decode instead of codeUnits and fromCharCodes, since codeUnits and fromCharCodes are UTF 16 * Bumped version number, updated change log, removed redundant constructors. * Didn’t need to switch this to UTF8 * Remove unused table in test --- CHANGELOG.md | 4 ++++ lib/src/client_messages.dart | 2 +- lib/src/postgresql_codec.dart | 6 +++--- lib/src/query.dart | 2 +- lib/src/server_messages.dart | 8 ++++---- pubspec.yaml | 2 +- test/encoding_test.dart | 19 ++++++++++--------- test/interpolation_test.dart | 3 ++- test/query_test.dart | 15 +++++++++++++++ 9 files changed, 41 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a20cf..cdec69d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.3 + +- Fixed issue with UTF8 encoding + ## 0.9.2 - Bump for documentation diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 014432f..48ba992 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -17,7 +17,7 @@ abstract class _ClientMessage { int get length; int applyStringToBuffer(String string, ByteData buffer, int offset) { - var postStringOffset = string.codeUnits.fold(offset, (idx, unit) { + var postStringOffset = UTF8.encode(string).fold(offset, (idx, unit) { buffer.setInt8(idx, unit); return idx + 1; }); diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart index 46dc95a..a566147 100644 --- a/lib/src/postgresql_codec.dart +++ b/lib/src/postgresql_codec.dart @@ -130,7 +130,7 @@ abstract class PostgreSQLCodec { } String val = value; - outBuffer = new Uint8List.fromList(val.codeUnits); + outBuffer = UTF8.encode(val); } else if (postgresType == TypeFloat4) { if (value is! double) { throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); @@ -207,7 +207,7 @@ abstract class PostgreSQLCodec { if (quoteCount == 0 && backslashCount == 0) { buf.write(text); } else { - text.codeUnits.forEach((i) { + UTF8.encode(text).forEach((i) { if (i == quoteCodeUnit || i == backslashCodeUnit) { buf.writeCharCode(i); buf.writeCharCode(i); @@ -354,7 +354,7 @@ abstract class PostgreSQLCodec { return new DateTime.utc(2000).add(new Duration(days: value.getInt32(0))); default: - return new String.fromCharCodes(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); + return UTF8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); } } } \ No newline at end of file diff --git a/lib/src/query.dart b/lib/src/query.dart index a7b2c20..2b0aaa6 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -163,7 +163,7 @@ class _ParameterValue { _ParameterValue.text(dynamic value) { isBinary = false; if (value != null) { - bytes = new Uint8List.fromList(PostgreSQLCodec.encode(value, escapeStrings: false).codeUnits); + bytes = UTF8.encode(PostgreSQLCodec.encode(value, escapeStrings: false)); } length = bytes?.length; } diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index c785004..2975855 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -60,8 +60,8 @@ class _ParameterStatusMessage extends _ServerMessage { String value; void readBytes(Uint8List bytes) { - name = new String.fromCharCodes(bytes.sublist(0, bytes.indexOf(0))); - value = new String.fromCharCodes(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + name = UTF8.decode(bytes.sublist(0, bytes.indexOf(0))); + value = UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } String toString() => "Parameter Message: $name $value"; @@ -75,7 +75,7 @@ class _ReadyForQueryMessage extends _ServerMessage { String state; void readBytes(Uint8List bytes) { - state = new String.fromCharCodes(bytes); + state = UTF8.decode(bytes); } String toString() => "Ready Message: $state"; @@ -144,7 +144,7 @@ class _CommandCompleteMessage extends _ServerMessage { static RegExp identifierExpression = new RegExp(r"[A-Z ]*"); void readBytes(Uint8List bytes) { - var str = new String.fromCharCodes(bytes.sublist(0, bytes.length - 1)); + var str = UTF8.decode(bytes.sublist(0, bytes.length - 1)); var match = identifierExpression.firstMatch(str); if (match.end < str.length) { diff --git a/pubspec.yaml b/pubspec.yaml index 1896446..3294a1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: A library to connect and query PostgreSQL databases. -version: 0.9.2 +version: 0.9.3 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 761eca8..bc4a586 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; import 'dart:typed_data'; @@ -45,31 +46,31 @@ void main() { test("Escape strings", () { // ' b o b ' - expect(PostgreSQLCodec.encode('bob').codeUnits, equals([39, 98, 111, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bob')), equals([39, 98, 111, 98, 39])); // ' b o \n b ' - expect(PostgreSQLCodec.encode('bo\nb').codeUnits, equals([39, 98, 111, 10, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bo\nb')), equals([39, 98, 111, 10, 98, 39])); // ' b o \r b ' - expect(PostgreSQLCodec.encode('bo\rb').codeUnits, equals([39, 98, 111, 13, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bo\rb')), equals([39, 98, 111, 13, 98, 39])); // ' b o \b b ' - expect(PostgreSQLCodec.encode('bo\bb').codeUnits, equals([39, 98, 111, 8, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bo\bb')), equals([39, 98, 111, 8, 98, 39])); // ' ' ' ' - expect(PostgreSQLCodec.encode("'").codeUnits, equals([39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("'")), equals([39, 39, 39, 39])); // ' ' ' ' ' ' - expect(PostgreSQLCodec.encode("''").codeUnits, equals([39, 39, 39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("''")), equals([39, 39, 39, 39, 39, 39])); // ' ' ' ' ' ' - expect(PostgreSQLCodec.encode("\''").codeUnits, equals([39, 39, 39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("\''")), equals([39, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' ' ' - expect(PostgreSQLCodec.encode("\\''").codeUnits, equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' - expect(PostgreSQLCodec.encode("\\'").codeUnits, equals([32, 69, 39, 92, 92, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); }); test("Encode DateTime", () { diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index b18cf3c..817faf5 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; @@ -36,6 +37,6 @@ void main() { var result = PostgreSQLFormat.substitute("@id:text @foo", {"id" : "1';select", "foo" : "3\\4"}); // ' 1 ' ' ; s e l e c t ' sp sp E ' 3 \ \ 4 ' - expect(result.codeUnits, [39,49,39,39,59,115,101,108,101,99,116,39, 32, 32,69,39,51,92,92,52,39]); + expect(UTF8.encode(result), [39,49,39,39,59,115,101,108,101,99,116,39, 32, 32,69,39,51,92,92,52,39]); }); } \ No newline at end of file diff --git a/test/query_test.dart b/test/query_test.dart index c9a09de..58f321b 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -17,6 +17,21 @@ void main() { await connection.close(); }); + test("UTF8 strings", () async { + var result = await connection.query("INSERT INTO t (t) values " + "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})" + "returning t", + substitutionValues: { + "t" : "°∆", + }); + + var expectedRow = ["°∆"]; + expect(result, [expectedRow]); + + result = await connection.query("select t from t"); + expect(result, [expectedRow]); + }); + test("Query without specifying types", () async { var result = await connection.query("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " "(${PostgreSQLFormat.id("i")}," From dfb7e4ad74d485f347f7b00512f5a560b4b08937 Mon Sep 17 00:00:00 2001 From: Alex Nachlas Date: Sun, 4 Dec 2016 13:15:55 -0500 Subject: [PATCH 03/73] Make sure buffers are the right length. (#7) * Make sure buffers are the right length. * Only encode strings once. * Make constant arrays static constants * dartfmt and moved some things into their own files * Moved around some comments. --- lib/postgres.dart | 3 +- lib/src/client_messages.dart | 214 ++++++++---- lib/src/connection.dart | 68 ++-- lib/src/connection_fsm.dart | 57 +-- lib/src/constants.dart | 26 ++ lib/src/exceptions.dart | 40 ++- lib/src/message_window.dart | 53 +-- lib/src/postgresql_codec.dart | 79 +++-- lib/src/query.dart | 66 ++-- lib/src/server_messages.dart | 30 +- lib/src/substituter.dart | 106 +++--- lib/src/transaction_proxy.dart | 27 +- lib/src/utf8_backed_string.dart | 23 ++ test/connection_test.dart | 200 ++++++++--- test/decode_test.dart | 52 +-- test/encoding_test.dart | 117 ++++--- test/interpolation_test.dart | 42 ++- test/query_reuse_test.dart | 602 ++++++++++++++++++++------------ test/query_test.dart | 249 ++++++++----- test/transaction_test.dart | 80 +++-- 20 files changed, 1420 insertions(+), 714 deletions(-) create mode 100644 lib/src/constants.dart create mode 100644 lib/src/utf8_backed_string.dart diff --git a/lib/postgres.dart b/lib/postgres.dart index f529c32..5dbe806 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -17,4 +17,5 @@ part 'src/message_window.dart'; part 'src/connection_fsm.dart'; part 'src/query.dart'; part 'src/exceptions.dart'; - +part 'src/constants.dart'; +part 'src/utf8_backed_string.dart'; diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 48ba992..77e23d1 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -16,8 +16,9 @@ abstract class _ClientMessage { int get length; - int applyStringToBuffer(String string, ByteData buffer, int offset) { - var postStringOffset = UTF8.encode(string).fold(offset, (idx, unit) { + int applyStringToBuffer( + UTF8BackedString string, ByteData buffer, int offset) { + var postStringOffset = string.utf8Bytes.fold(offset, (idx, unit) { buffer.setInt8(idx, unit); return idx + 1; }); @@ -26,6 +27,15 @@ abstract class _ClientMessage { return postStringOffset + 1; } + int applyBytesToBuffer(List bytes, ByteData buffer, int offset) { + var postStringOffset = bytes.fold(offset, (idx, unit) { + buffer.setInt8(idx, unit); + return idx + 1; + }); + + return postStringOffset; + } + int applyToBuffer(ByteData aggregateBuffer, int offsetIntoAggregateBuffer); Uint8List asBytes() { @@ -35,53 +45,66 @@ abstract class _ClientMessage { } static Uint8List aggregateBytes(List<_ClientMessage> messages) { - var totalLength = messages.fold(0, (total, _ClientMessage next) => total + next.length); + var totalLength = + messages.fold(0, (total, _ClientMessage next) => total + next.length); var buffer = new ByteData(totalLength); var offset = 0; - messages.fold(offset, (inOffset, msg) => msg.applyToBuffer(buffer, inOffset)); + messages.fold( + offset, (inOffset, msg) => msg.applyToBuffer(buffer, inOffset)); return buffer.buffer.asUint8List(); } } class _StartupMessage extends _ClientMessage { - _StartupMessage(this.databaseName, this.timeZone, {this.username}); + _StartupMessage(String databaseName, String timeZone, {String username}) { + this.databaseName = new UTF8BackedString(databaseName); + this.timeZone = new UTF8BackedString(timeZone); + if (username != null) { + this.username = new UTF8BackedString(username); + } + } - String username = null; - String databaseName; - String timeZone; + UTF8BackedString username = null; + UTF8BackedString databaseName; + UTF8BackedString timeZone; ByteData buffer; int get length { var fixedLength = 53; - var variableLength = (username?.length ?? 0) - + databaseName.length - + timeZone.length + 3; + var variableLength = (username?.utf8Length ?? 0) + + databaseName.utf8Length + + timeZone.utf8Length + + 3; return fixedLength + variableLength; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setInt32(offset, length); offset += 4; - buffer.setInt32(offset, _ClientMessage.ProtocolVersion); offset += 4; + buffer.setInt32(offset, length); + offset += 4; + buffer.setInt32(offset, _ClientMessage.ProtocolVersion); + offset += 4; if (username != null) { - offset = applyStringToBuffer("user", buffer, offset); + offset = applyBytesToBuffer((UTF8ByteConstants.user), buffer, offset); offset = applyStringToBuffer(username, buffer, offset); } - offset = applyStringToBuffer("database", buffer, offset); + offset = applyBytesToBuffer(UTF8ByteConstants.database, buffer, offset); offset = applyStringToBuffer(databaseName, buffer, offset); - offset = applyStringToBuffer("client_encoding", buffer, offset); - offset = applyStringToBuffer("UTF8", buffer, offset); + offset = + applyBytesToBuffer(UTF8ByteConstants.clientEncoding, buffer, offset); + offset = applyBytesToBuffer(UTF8ByteConstants.utf8, buffer, offset); - offset = applyStringToBuffer("TimeZone", buffer, offset); + offset = applyBytesToBuffer(UTF8ByteConstants.timeZone, buffer, offset); offset = applyStringToBuffer(timeZone, buffer, offset); - buffer.setInt8(offset, 0); offset += 1; + buffer.setInt8(offset, 0); + offset += 1; return offset; } @@ -89,20 +112,24 @@ class _StartupMessage extends _ClientMessage { class _AuthMD5Message extends _ClientMessage { _AuthMD5Message(String username, String password, List saltBytes) { - var passwordHash = md5.convert("${password}${username}".codeUnits).toString(); + var passwordHash = + md5.convert("${password}${username}".codeUnits).toString(); var saltString = new String.fromCharCodes(saltBytes); - hashedAuthString = "md5" + md5.convert("$passwordHash$saltString".codeUnits).toString(); + hashedAuthString = new UTF8BackedString( + "md5" + md5.convert("$passwordHash$saltString".codeUnits).toString()); } - String hashedAuthString; + UTF8BackedString hashedAuthString; int get length { - return 6 + hashedAuthString.length; + return 6 + hashedAuthString.utf8Length; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.PasswordIdentifier); offset += 1; - buffer.setUint32(offset, length - 1); offset += 4; + buffer.setUint8(offset, _ClientMessage.PasswordIdentifier); + offset += 1; + buffer.setUint32(offset, length - 1); + offset += 4; offset = applyStringToBuffer(hashedAuthString, buffer, offset); return offset; @@ -110,17 +137,21 @@ class _AuthMD5Message extends _ClientMessage { } class _QueryMessage extends _ClientMessage { - _QueryMessage(this.queryString); + _QueryMessage(String queryString) { + this.queryString = new UTF8BackedString(queryString); + } - String queryString; + UTF8BackedString queryString; int get length { - return 6 + queryString.length; + return 6 + queryString.utf8Length; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.QueryIdentifier); offset += 1; - buffer.setUint32(offset, length - 1); offset += 4; + buffer.setUint8(offset, _ClientMessage.QueryIdentifier); + offset += 1; + buffer.setUint32(offset, length - 1); + offset += 4; offset = applyStringToBuffer(queryString, buffer, offset); return offset; @@ -128,55 +159,71 @@ class _QueryMessage extends _ClientMessage { } class _ParseMessage extends _ClientMessage { - _ParseMessage (this.statement, {this.statementName: ""}); + _ParseMessage(String statement, {String statementName: ""}) { + this.statement = new UTF8BackedString(statement); + this.statementName = new UTF8BackedString(statementName); + } - String statementName = ""; - String statement; + UTF8BackedString statementName; + UTF8BackedString statement; int get length { - return 9 + statement.length + statementName.length; + return 9 + statement.utf8Length + statementName.utf8Length; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.ParseIdentifier); offset += 1; - buffer.setUint32(offset, length - 1); offset += 4; - offset = applyStringToBuffer(statementName, buffer, offset); // Name of prepared statement + buffer.setUint8(offset, _ClientMessage.ParseIdentifier); + offset += 1; + buffer.setUint32(offset, length - 1); + offset += 4; + // Name of prepared statement + offset = applyStringToBuffer(statementName, buffer, offset); offset = applyStringToBuffer(statement, buffer, offset); // Query string - buffer.setUint16(offset, 0); offset += 2; // Specifying types - may add this in the future, for now indicating we want the backend to infer. + buffer.setUint16(offset, 0); + // Specifying types - may add this in the future, for now indicating we want the backend to infer. + offset += 2; return offset; } } class _DescribeMessage extends _ClientMessage { - _DescribeMessage({this.statementName: ""}); + _DescribeMessage({String statementName: ""}) { + this.statementName = new UTF8BackedString(statementName); + } - String statementName = ""; + UTF8BackedString statementName; int get length { - return 7 + statementName.length; + return 7 + statementName.utf8Length; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.DescribeIdentifier); offset += 1; - buffer.setUint32(offset, length - 1); offset += 4; - buffer.setUint8(offset, 83); offset += 1; // Indicate we are referencing a prepared statement - offset = applyStringToBuffer(statementName, buffer, offset); // Name of prepared statement + buffer.setUint8(offset, _ClientMessage.DescribeIdentifier); + offset += 1; + buffer.setUint32(offset, length - 1); + offset += 4; + buffer.setUint8(offset, 83); + offset += 1; // Indicate we are referencing a prepared statement + offset = applyStringToBuffer( + statementName, buffer, offset); // Name of prepared statement return offset; } } class _BindMessage extends _ClientMessage { - _BindMessage(this.parameters, {this.statementName: ""}) { + _BindMessage(this.parameters, {String statementName: ""}) { typeSpecCount = parameters.where((p) => p.isBinary).length; + this.statementName = new UTF8BackedString(statementName); } List<_ParameterValue> parameters; - String statementName; + UTF8BackedString statementName; int typeSpecCount; int _cachedLength; + int get length { if (_cachedLength == null) { var inputParameterElementCount = parameters.length; @@ -185,7 +232,7 @@ class _BindMessage extends _ClientMessage { } _cachedLength = 15; - _cachedLength += statementName.length; + _cachedLength += statementName.utf8Length; _cachedLength += inputParameterElementCount * 2; _cachedLength += parameters.fold(0, (len, _ParameterValue paramValue) { if (paramValue.bytes == null) { @@ -199,35 +246,54 @@ class _BindMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.BindIdentifier); offset += 1; - buffer.setUint32(offset, length - 1); offset += 4; + buffer.setUint8(offset, _ClientMessage.BindIdentifier); + offset += 1; + buffer.setUint32(offset, length - 1); + offset += 4; - offset = applyStringToBuffer("", buffer, offset); // Name of portal - currently unnamed portal. - offset = applyStringToBuffer(statementName, buffer, offset); // Name of prepared statement. + // Name of portal - currently unnamed portal. + offset = applyBytesToBuffer([0], buffer, offset); + // Name of prepared statement. + offset = applyStringToBuffer(statementName, buffer, offset); // OK, if we have no specified types at all, we can use 0. If we have all specified types, we can use 1. If we have a mix, we have to individually // call out each type. if (typeSpecCount == parameters.length) { - buffer.setUint16(offset, 1); offset += 2; // Apply following format code for all parameters by indicating 1 - buffer.setUint16(offset, _ClientMessage.FormatBinary); offset += 2; // Specify format code for all params is BINARY + buffer.setUint16(offset, 1); + // Apply following format code for all parameters by indicating 1 + offset += 2; + buffer.setUint16(offset, _ClientMessage.FormatBinary); + offset += 2; // Specify format code for all params is BINARY } else if (typeSpecCount == 0) { - buffer.setUint16(offset, 1); offset += 2; // Apply following format code for all parameters by indicating 1 - buffer.setUint16(offset, _ClientMessage.FormatText); offset += 2; // Specify format code for all params is TEXT + buffer.setUint16(offset, 1); + // Apply following format code for all parameters by indicating 1 + offset += 2; + buffer.setUint16(offset, _ClientMessage.FormatText); + offset += 2; // Specify format code for all params is TEXT } else { // Well, we have some text and some binary, so we have to be explicit about each one - buffer.setUint16(offset, parameters.length); offset += 2; + buffer.setUint16(offset, parameters.length); + offset += 2; parameters.forEach((p) { - buffer.setUint16(offset, p.isBinary ? _ClientMessage.FormatBinary : _ClientMessage.FormatText); offset += 2; + buffer.setUint16( + offset, + p.isBinary + ? _ClientMessage.FormatBinary + : _ClientMessage.FormatText); + offset += 2; }); } // This must be the number of $n's in the query. - buffer.setUint16(offset, parameters.length); offset += 2; // Number of parameters specified by query + buffer.setUint16(offset, parameters.length); + offset += 2; // Number of parameters specified by query parameters.forEach((p) { if (p.bytes == null) { - buffer.setInt32(offset, -1); offset += 4; + buffer.setInt32(offset, -1); + offset += 4; } else { - buffer.setInt32(offset, p.length); offset += 4; + buffer.setInt32(offset, p.length); + offset += 4; offset = p.bytes.fold(offset, (inOffset, byte) { buffer.setUint8(inOffset, byte); return inOffset + 1; @@ -236,8 +302,10 @@ class _BindMessage extends _ClientMessage { }); // Result columns - we always want binary for all of them, so specify 1:1. - buffer.setUint16(offset, 1); offset += 2; // Apply format code for all result values by indicating 1 - buffer.setUint16(offset, 1); offset += 2; // Specify format code for all result values in binary + buffer.setUint16(offset, 1); + offset += 2; // Apply format code for all result values by indicating 1 + buffer.setUint16(offset, 1); + offset += 2; // Specify format code for all result values in binary return offset; } @@ -251,10 +319,13 @@ class _ExecuteMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.ExecuteIdentifier); offset += 1; - buffer.setUint32(offset, length - 1); offset += 4; - offset = applyStringToBuffer("", buffer, offset); // Portal name - buffer.setUint32(offset, 0); offset += 4; // Row limit + buffer.setUint8(offset, _ClientMessage.ExecuteIdentifier); + offset += 1; + buffer.setUint32(offset, length - 1); + offset += 4; + offset = applyBytesToBuffer([0], buffer, offset); // Portal name + buffer.setUint32(offset, 0); + offset += 4; // Row limit return offset; } @@ -268,9 +339,10 @@ class _SyncMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.SyncIdentifier); offset += 1; - buffer.setUint32(offset, 4); offset += 4; - + buffer.setUint8(offset, _ClientMessage.SyncIdentifier); + offset += 1; + buffer.setUint32(offset, 4); + offset += 4; return offset; } -} \ No newline at end of file +} diff --git a/lib/src/connection.dart b/lib/src/connection.dart index d721d1d..0494849 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -21,7 +21,8 @@ abstract class PostgreSQLExecutionContext { /// By default, instances of this class will reuse queries. This allows significantly more efficient transport to and from the database. You do not have to do /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. - Future>> query(String fmtString, {Map substitutionValues: null, bool allowReuse: true}); + Future>> query(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}); /// Executes a query on this context. /// @@ -30,7 +31,8 @@ abstract class PostgreSQLExecutionContext { /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. - Future execute(String fmtString, {Map substitutionValues: null}); + Future execute(String fmtString, + {Map substitutionValues: null}); /// Cancels a transaction on this context. /// @@ -44,7 +46,6 @@ abstract class PostgreSQLExecutionContext { /// The primary type of this library, a connection is responsible for connecting to databases and executing queries. /// A connection may be opened with [open] after it is created. class PostgreSQLConnection implements PostgreSQLExecutionContext { - /// Creates an instance of [PostgreSQLConnection]. /// /// [host] must be a hostname, e.g. "foobar.com" or IP address. Do not include scheme or port. @@ -54,7 +55,12 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// [timeoutInSeconds] refers to the amount of time [PostgreSQLConnection] will wait while establishing a connection before it gives up. /// [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: null, this.password: null, this.timeoutInSeconds: 30, this.timeZone: "UTC", this.useSSL: false}) { + PostgreSQLConnection(this.host, this.port, this.databaseName, + {this.username: null, + this.password: null, + this.timeoutInSeconds: 30, + this.timeZone: "UTC", + this.useSSL: false}) { _connectionState = new _PostgreSQLConnectionStateClosed(); _connectionState.connection = this; } @@ -112,6 +118,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _PostgreSQLConnectionState _connectionState; List<_Query> _queryQueue = []; + _Query get _pendingQuery { if (_queryQueue.isEmpty) { return null; @@ -128,29 +135,38 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// 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 { if (_hasConnectedPreviously) { - throw new PostgreSQLException("Attempting to reopen a closed connection. Create a new instance instead."); + throw new PostgreSQLException( + "Attempting to reopen a closed connection. Create a new instance instead."); } _hasConnectedPreviously = true; if (useSSL) { - _socket = await SecureSocket.connect(host, port).timeout(new Duration(seconds: timeoutInSeconds)); + _socket = await SecureSocket + .connect(host, port) + .timeout(new Duration(seconds: timeoutInSeconds)); } else { - _socket = await Socket.connect(host, port).timeout(new Duration(seconds: timeoutInSeconds)); + _socket = await Socket + .connect(host, port) + .timeout(new Duration(seconds: timeoutInSeconds)); } _framer = new _MessageFramer(); - _socket.listen(_readData, onError: _handleSocketError, onDone: _handleSocketClosed); + _socket.listen(_readData, + onError: _handleSocketError, onDone: _handleSocketClosed); var connectionComplete = new Completer(); - _transitionToState(new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); + _transitionToState( + new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); - return connectionComplete.future.timeout(new Duration(seconds: timeoutInSeconds), onTimeout: () { + return connectionComplete.future + .timeout(new Duration(seconds: timeoutInSeconds), onTimeout: () { _connectionState = new _PostgreSQLConnectionStateClosed(); _socket?.destroy(); _cancelCurrentQueries(); - throw new PostgreSQLException("Timed out trying to connect to database postgres://$host:$port/$databaseName."); + throw new PostgreSQLException( + "Timed out trying to connect to database postgres://$host:$port/$databaseName."); }); } @@ -186,12 +202,16 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. /// - Future>> query(String fmtString, {Map substitutionValues: null, bool allowReuse: true}) async { + Future>> query(String fmtString, + {Map substitutionValues: null, + bool allowReuse: true}) async { if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } - var query = new _Query>>(fmtString, substitutionValues, this, null); + var query = new _Query>>( + fmtString, substitutionValues, this, null); if (allowReuse) { query.statementIdentifier = _reuseIdentifierForQuery(query); } @@ -206,9 +226,11 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. - Future execute(String fmtString, {Map substitutionValues: null}) async { + Future execute(String fmtString, + {Map substitutionValues: null}) async { if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } var query = new _Query(fmtString, substitutionValues, this, null) @@ -244,9 +266,11 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// ctx.query("INSERT INTO t (id) VALUES (2)"); /// } /// }); - Future transaction(Future queryBlock(PostgreSQLExecutionContext connection)) async { + Future transaction( + Future queryBlock(PostgreSQLExecutionContext connection)) async { if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } var proxy = new _TransactionProxy(this, queryBlock); @@ -289,7 +313,8 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { // get the error and not the close message, since completeError is // synchronous. scheduleMicrotask(() { - var exception = new PostgreSQLException("Connection closed or query cancelled."); + var exception = + new PostgreSQLException("Connection closed or query cancelled."); queries?.forEach((q) { q.completeError(exception); }); @@ -372,7 +397,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { var string = "$_reuseCounter".padLeft(12, "0"); - _reuseCounter ++; + _reuseCounter++; return string; } @@ -380,5 +405,6 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { class _TransactionRollbackException implements Exception { _TransactionRollbackException(this.reason); + String reason; -} \ No newline at end of file +} diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 4f48d20..857c6e4 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -18,35 +18,37 @@ abstract class _PostgreSQLConnectionState { _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); - if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { + if (exception.severity == PostgreSQLSeverity.fatal || + exception.severity == PostgreSQLSeverity.panic) { return new _PostgreSQLConnectionStateClosed(); } return this; } - void onExit() { - - } + void onExit() {} } /* Closed State; starts here and ends here. */ -class _PostgreSQLConnectionStateClosed extends _PostgreSQLConnectionState { -} +class _PostgreSQLConnectionStateClosed extends _PostgreSQLConnectionState {} /* Socket connected, prior to any PostgreSQL handshaking - initiates that handshaking */ -class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateSocketConnected + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateSocketConnected(this.completer); + Completer completer; _PostgreSQLConnectionState onEnter() { - var startupMessage = new _StartupMessage(connection.databaseName, connection.timeZone, username: connection.username); + var startupMessage = new _StartupMessage( + connection.databaseName, connection.timeZone, + username: connection.username); connection._socket.add(startupMessage.asBytes()); @@ -73,7 +75,9 @@ class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionSta return new _PostgreSQLConnectionStateAuthenticating(completer); } - completer.completeError(new PostgreSQLException("Unsupported authentication type ${authMessage.type}, closing connection.")); + completer.completeError( + new PostgreSQLException("Unsupported authentication type ${authMessage + .type}, closing connection.")); return new _PostgreSQLConnectionStateClosed(); } @@ -83,12 +87,15 @@ class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionSta Authenticating state */ -class _PostgreSQLConnectionStateAuthenticating extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateAuthenticating + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateAuthenticating(this.completer); + Completer completer; _PostgreSQLConnectionState onEnter() { - var authMessage = new _AuthMD5Message(connection.username, connection.password, connection._salt); + var authMessage = new _AuthMD5Message( + connection.username, connection.password, connection._salt); connection._socket.add(authMessage.asBytes()); @@ -123,8 +130,10 @@ class _PostgreSQLConnectionStateAuthenticating extends _PostgreSQLConnectionStat Authenticated state */ -class _PostgreSQLConnectionStateAuthenticated extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateAuthenticated + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateAuthenticated(this.completer); + Completer completer; _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { @@ -219,7 +228,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { var exception = new PostgreSQLException._(message.fields); returningException ??= exception; - if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { + if (exception.severity == PostgreSQLSeverity.fatal || + exception.severity == PostgreSQLSeverity.panic) { return new _PostgreSQLConnectionStateClosed(); } @@ -248,11 +258,13 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { query.complete(rowsAffected); } - return new _PostgreSQLConnectionStateReadyInTransaction(query.transaction); + return new _PostgreSQLConnectionStateReadyInTransaction( + query.transaction); } else if (message.state == _ReadyForQueryMessage.StateTransactionError) { // This should cancel the transaction, we may have to send a commit here query.completeError(returningException); - return new _PostgreSQLConnectionStateTransactionFailure(query.transaction); + return new _PostgreSQLConnectionStateTransactionFailure( + query.transaction); } } else if (message is _CommandCompleteMessage) { rowsAffected = message.rowsAffected; @@ -261,7 +273,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } else if (message is _DataRowMessage) { query.addRow(message.values); } else if (message is _ParameterDescriptionMessage) { - var validationException = query.validateParameters(message.parameterTypeIDs); + var validationException = + query.validateParameters(message.parameterTypeIDs); if (validationException != null) { query.cache = null; } @@ -274,7 +287,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { /* Idle Transaction State */ -class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateReadyInTransaction + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateReadyInTransaction(this.transaction); _TransactionProxy transaction; @@ -318,8 +332,10 @@ class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnection Transaction error state */ -class _PostgreSQLConnectionStateTransactionFailure extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateTransactionFailure + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateTransactionFailure(this.transaction); + _TransactionProxy transaction; _PostgreSQLConnectionState awake() { @@ -327,10 +343,9 @@ class _PostgreSQLConnectionStateTransactionFailure extends _PostgreSQLConnection } } - /* Hack for deferred error */ -class _PostgreSQLConnectionStateDeferredFailure extends _PostgreSQLConnectionState { -} +class _PostgreSQLConnectionStateDeferredFailure + extends _PostgreSQLConnectionState {} diff --git a/lib/src/constants.dart b/lib/src/constants.dart new file mode 100644 index 0000000..bb3533c --- /dev/null +++ b/lib/src/constants.dart @@ -0,0 +1,26 @@ +part of postgres; + +class UTF8ByteConstants { + static const user = const [117, 115, 101, 114, 0]; + static const database = const [100, 97, 116, 97, 98, 97, 115, 101, 0]; + static const clientEncoding = const [ + 99, + 108, + 105, + 101, + 110, + 116, + 95, + 101, + 110, + 99, + 111, + 100, + 105, + 110, + 103, + 0 + ]; + static const utf8 = const [85, 84, 70, 56, 0]; + static const timeZone = const [84, 105, 109, 101, 90, 111, 110, 101, 0]; +} diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index bdc7efb..0e1b5bb 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -4,7 +4,6 @@ part of postgres; /// /// [panic] and [fatal] errors will close the connection. enum PostgreSQLSeverity { - /// A [PostgreSQLException] with this severity indicates the throwing connection is now closed. panic, @@ -35,16 +34,21 @@ enum PostgreSQLSeverity { /// Exception thrown by [PostgreSQLConnection] instances. class PostgreSQLException implements Exception { - PostgreSQLException(String message, {PostgreSQLSeverity severity: PostgreSQLSeverity.error, this.stackTrace}) { + PostgreSQLException(String message, + {PostgreSQLSeverity severity: PostgreSQLSeverity.error, + this.stackTrace}) { this.severity = severity; this.message = message; code = ""; } PostgreSQLException._(List<_ErrorField> errorFields, {this.stackTrace}) { - var finder = (int identifer) => (errorFields.firstWhere((_ErrorField e) => e.identificationToken == identifer, orElse: () => null)); + var finder = (int identifer) => (errorFields.firstWhere( + (_ErrorField e) => e.identificationToken == identifer, + orElse: () => null)); - severity = _ErrorField.severityFromString(finder(_ErrorField.SeverityIdentifier).text); + severity = _ErrorField + .severityFromString(finder(_ErrorField.SeverityIdentifier).text); code = finder(_ErrorField.CodeIdentifier).text; message = finder(_ErrorField.MessageIdentifier).text; detail = finder(_ErrorField.DetailIdentifier)?.text; @@ -107,7 +111,8 @@ class PostgreSQLException implements Exception { /// A [StackTrace] if available. StackTrace stackTrace; - String toString() => "$severity $code: $message Detail: $detail Hint: $hint Table: $tableName Column: $columnName Constraint: $constraintName"; + String toString() => + "$severity $code: $message Detail: $detail Hint: $hint Table: $tableName Column: $columnName Constraint: $constraintName"; } class _ErrorField { @@ -131,18 +136,27 @@ class _ErrorField { static PostgreSQLSeverity severityFromString(String str) { switch (str) { - case "ERROR" : return PostgreSQLSeverity.error; - case "FATAL" : return PostgreSQLSeverity.fatal; - case "PANIC" : return PostgreSQLSeverity.panic; - case "WARNING" : return PostgreSQLSeverity.warning; - case "NOTICE" : return PostgreSQLSeverity.notice; - case "DEBUG" : return PostgreSQLSeverity.debug; - case "INFO" : return PostgreSQLSeverity.info; - case "LOG" : return PostgreSQLSeverity.log; + case "ERROR": + return PostgreSQLSeverity.error; + case "FATAL": + return PostgreSQLSeverity.fatal; + case "PANIC": + return PostgreSQLSeverity.panic; + case "WARNING": + return PostgreSQLSeverity.warning; + case "NOTICE": + return PostgreSQLSeverity.notice; + case "DEBUG": + return PostgreSQLSeverity.debug; + case "INFO": + return PostgreSQLSeverity.info; + case "LOG": + return PostgreSQLSeverity.log; } return PostgreSQLSeverity.unknown; } + int identificationToken; String get text => _buffer.toString(); diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 2512daa..45691f2 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -2,21 +2,18 @@ part of postgres; class _MessageFrame { static Map _messageTypeMap = { - 49 : () => new _ParseCompleteMessage(), - 50 : () => new _BindCompleteMessage(), - 67 : () => new _CommandCompleteMessage(), - 68 : () => new _DataRowMessage(), - 69 : () => new _ErrorResponseMessage(), - - 75 : () => new _BackendKeyMessage(), - - 82 : () => new _AuthenticationMessage(), - 83 : () => new _ParameterStatusMessage(), - 84 : () => new _RowDescriptionMessage(), - - 90 : () => new _ReadyForQueryMessage(), - 110 : () => new _NoDataMessage(), - 116 : () => new _ParameterDescriptionMessage() + 49: () => new _ParseCompleteMessage(), + 50: () => new _BindCompleteMessage(), + 67: () => new _CommandCompleteMessage(), + 68: () => new _DataRowMessage(), + 69: () => new _ErrorResponseMessage(), + 75: () => new _BackendKeyMessage(), + 82: () => new _AuthenticationMessage(), + 83: () => new _ParameterStatusMessage(), + 84: () => new _RowDescriptionMessage(), + 90: () => new _ReadyForQueryMessage(), + 110: () => new _NoDataMessage(), + 116: () => new _ParameterDescriptionMessage() }; BytesBuilder _inputBuffer = new BytesBuilder(copy: false); @@ -29,7 +26,6 @@ class _MessageFrame { int addBytes(Uint8List bytes) { // If we just have the beginning of a packet, then consume the bytes and continue. if (_inputBuffer.length + bytes.length < 5) { - _inputBuffer.add(bytes); return bytes.length; } @@ -44,21 +40,28 @@ class _MessageFrame { var takenBytes = _inputBuffer.takeBytes(); headerBuffer.setRange(0, takenBytes.length, takenBytes); } - headerBuffer.setRange(5 - countNeededFromIncomingToDetermineMessage, 5, new Uint8List.view(bytes.buffer, bytes.offsetInBytes, countNeededFromIncomingToDetermineMessage)); + headerBuffer.setRange( + 5 - countNeededFromIncomingToDetermineMessage, + 5, + new Uint8List.view(bytes.buffer, bytes.offsetInBytes, + countNeededFromIncomingToDetermineMessage)); var bufReader = new ByteData.view(headerBuffer.buffer); type = bufReader.getUint8(0); - expectedLength = bufReader.getUint32(1) - 4; // Remove this length from the length needed to complete this message + // Remove this length from the length needed to complete this message + expectedLength = bufReader.getUint32(1) - 4; var offsetIntoIncomingBytes = countNeededFromIncomingToDetermineMessage; var byteBufferLengthRemaining = bytes.length - offsetIntoIncomingBytes; if (byteBufferLengthRemaining >= expectedLength) { - _inputBuffer.add(new Uint8List.view(bytes.buffer, bytes.offsetInBytes + offsetIntoIncomingBytes, expectedLength)); + _inputBuffer.add(new Uint8List.view(bytes.buffer, + bytes.offsetInBytes + offsetIntoIncomingBytes, expectedLength)); data = _inputBuffer.takeBytes(); return offsetIntoIncomingBytes + expectedLength; } - _inputBuffer.add(new Uint8List.view(bytes.buffer, bytes.offsetInBytes + offsetIntoIncomingBytes)); + _inputBuffer.add(new Uint8List.view( + bytes.buffer, bytes.offsetInBytes + offsetIntoIncomingBytes)); return bytes.length; } @@ -66,8 +69,7 @@ class _MessageFrame { var msgMaker = _messageTypeMap[type]; if (msgMaker == null) { msgMaker = () { - var msg = new _UnknownMessage() - ..code = type; + var msg = new _UnknownMessage()..code = type; return msg; }; } @@ -87,8 +89,9 @@ class _MessageFramer { void addBytes(Uint8List bytes) { var offsetIntoBytesRead = 0; - do { - offsetIntoBytesRead += messageInProgress.addBytes(new Uint8List.view(bytes.buffer, offsetIntoBytesRead)); + do { + offsetIntoBytesRead += messageInProgress + .addBytes(new Uint8List.view(bytes.buffer, offsetIntoBytesRead)); if (messageInProgress.isComplete) { messageQueue.add(messageInProgress); @@ -102,4 +105,4 @@ class _MessageFramer { _MessageFrame popMessage() { return messageQueue.removeAt(0); } -} \ No newline at end of file +} diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart index a566147..db0a2a9 100644 --- a/lib/src/postgresql_codec.dart +++ b/lib/src/postgresql_codec.dart @@ -52,7 +52,8 @@ abstract class PostgreSQLCodec { static const int TypeTimestamp = 1114; static const int TypeTimestampTZ = 1184; - static String encode(dynamic value, {PostgreSQLDataType dataType: null, bool escapeStrings: true}) { + static String encode(dynamic value, + {PostgreSQLDataType dataType: null, bool escapeStrings: true}) { if (value == null) { return "null"; } @@ -94,7 +95,9 @@ abstract class PostgreSQLCodec { if (postgresType == TypeBool) { if (value is! bool) { - throw new FormatException("Invalid type for parameter value. Expected: bool Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: bool Got: ${value + .runtimeType}"); } var bd = new ByteData(1); @@ -102,7 +105,9 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeInt8) { if (value is! int) { - throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value + .runtimeType}"); } var bd = new ByteData(8); @@ -110,7 +115,9 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeInt2) { if (value is! int) { - throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value + .runtimeType}"); } var bd = new ByteData(2); @@ -118,7 +125,9 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeInt4) { if (value is! int) { - throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value + .runtimeType}"); } var bd = new ByteData(4); @@ -126,14 +135,18 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeText) { if (value is! String) { - throw new FormatException("Invalid type for parameter value. Expected: String Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: String Got: ${value + .runtimeType}"); } String val = value; outBuffer = UTF8.encode(val); } else if (postgresType == TypeFloat4) { if (value is! double) { - throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: double Got: ${value + .runtimeType}"); } var bd = new ByteData(4); @@ -141,7 +154,9 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeFloat8) { if (value is! double) { - throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: double Got: ${value + .runtimeType}"); } var bd = new ByteData(8); @@ -149,7 +164,9 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeDate) { if (value is! DateTime) { - throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value + .runtimeType}"); } var bd = new ByteData(4); @@ -157,7 +174,9 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeTimestamp) { if (value is! DateTime) { - throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value + .runtimeType}"); } var bd = new ByteData(8); @@ -166,11 +185,14 @@ abstract class PostgreSQLCodec { outBuffer = bd.buffer.asUint8List(); } else if (postgresType == TypeTimestampTZ) { if (value is! DateTime) { - throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value + .runtimeType}"); } var bd = new ByteData(8); - bd.setInt64(0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); + bd.setInt64( + 0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); outBuffer = bd.buffer.asUint8List(); } @@ -190,9 +212,9 @@ abstract class PostgreSQLCodec { var it = new RuneIterator(text); while (it.moveNext()) { if (it.current == backslashCodeUnit) { - backslashCount ++; + backslashCount++; } else if (it.current == quoteCodeUnit) { - quoteCount ++; + quoteCount++; } } @@ -224,7 +246,8 @@ abstract class PostgreSQLCodec { static String encodeNumber(dynamic value) { if (value is! num) { - throw new PostgreSQLException("Trying to encode ${value.runtimeType}: $value as integer-like type."); + throw new PostgreSQLException("Trying to encode ${value + .runtimeType}: $value as integer-like type."); } if (value.isNaN) { @@ -240,7 +263,8 @@ abstract class PostgreSQLCodec { static String encodeDouble(dynamic value) { if (value is! num) { - throw new PostgreSQLException("Trying to encode ${value.runtimeType}: $value as double-like type."); + throw new PostgreSQLException( + "Trying to encode ${value.runtimeType}: $value as double-like type."); } if (value.isNaN) { @@ -256,7 +280,8 @@ abstract class PostgreSQLCodec { static String encodeBoolean(dynamic value) { if (value is! bool) { - throw new PostgreSQLException("Trying to encode ${value.runtimeType}: $value as boolean type."); + throw new PostgreSQLException( + "Trying to encode ${value.runtimeType}: $value as boolean type."); } return value ? "TRUE" : "FALSE"; @@ -264,7 +289,8 @@ abstract class PostgreSQLCodec { static String encodeDateTime(dynamic value, {bool isDateOnly: false}) { if (value is! DateTime) { - throw new PostgreSQLException("Trying to encode ${value.runtimeType}: $value as date-like type."); + throw new PostgreSQLException( + "Trying to encode ${value.runtimeType}: $value as date-like type."); } var string = value.toIso8601String(); @@ -277,7 +303,8 @@ abstract class PostgreSQLCodec { var timezoneMinuteOffset = value.timeZoneOffset.inMinutes % 60; var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, "0"); - var minuteComponent = timezoneMinuteOffset.abs().toString().padLeft(2, "0"); + var minuteComponent = + timezoneMinuteOffset.abs().toString().padLeft(2, "0"); if (timezoneHourOffset >= 0) { hourComponent = "+${hourComponent}"; @@ -324,7 +351,8 @@ abstract class PostgreSQLCodec { return encodeBoolean(value); } - throw new PostgreSQLException("Unknown inferred datatype from ${value.runtimeType}: $value"); + throw new PostgreSQLException( + "Unknown inferred datatype from ${value.runtimeType}: $value"); } static dynamic decodeValue(ByteData value, int dbTypeCode) { @@ -348,13 +376,16 @@ abstract class PostgreSQLCodec { case TypeTimestamp: case TypeTimestampTZ: - return new DateTime.utc(2000).add(new Duration(microseconds: value.getInt64(0))); + return new DateTime.utc(2000) + .add(new Duration(microseconds: value.getInt64(0))); case TypeDate: - return new DateTime.utc(2000).add(new Duration(days: value.getInt32(0))); + return new DateTime.utc(2000) + .add(new Duration(days: value.getInt32(0))); default: - return UTF8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); + return UTF8.decode( + value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); } } -} \ No newline at end of file +} diff --git a/lib/src/query.dart b/lib/src/query.dart index 2b0aaa6..500ca3c 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -1,11 +1,13 @@ part of postgres; class _Query { - _Query(this.statement, this.substitutionValues, this.connection, this.transaction); + _Query(this.statement, this.substitutionValues, this.connection, + this.transaction); bool onlyReturnAffectedRowCount = false; String statementIdentifier; Completer onComplete = new Completer.sync(); + Future get future => onComplete.future; final String statement; @@ -16,7 +18,9 @@ class _Query { List specifiedParameterTypeCodes; List<_FieldDescription> _fieldDescriptions; + List<_FieldDescription> get fieldDescriptions => _fieldDescriptions; + void set fieldDescriptions(List<_FieldDescription> fds) { _fieldDescriptions = fds; cache?.fieldDescriptions = fds; @@ -43,15 +47,15 @@ class _Query { String statementName = (statementIdentifier ?? ""); var formatIdentifiers = <_PostgreSQLFormatIdentifier>[]; - var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues, replace: (_PostgreSQLFormatIdentifier identifier, int index) { + var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues, + replace: (_PostgreSQLFormatIdentifier identifier, int index) { formatIdentifiers.add(identifier); return "\$$index"; }); - specifiedParameterTypeCodes = formatIdentifiers - .map((i) => i.typeCode) - .toList(); + specifiedParameterTypeCodes = + formatIdentifiers.map((i) => i.typeCode).toList(); var parameterList = formatIdentifiers .map((id) => encodeParameter(id, substitutionValues)) @@ -72,7 +76,8 @@ class _Query { socket.add(_ClientMessage.aggregateBytes(messages)); } - void sendCachedQuery(Socket socket, _QueryCache cacheQuery, Map substitutionValues) { + void sendCachedQuery(Socket socket, _QueryCache cacheQuery, + Map substitutionValues) { var statementName = cacheQuery.preparedStatementName; var parameterList = cacheQuery.orderedParameters .map((identifier) => encodeParameter(identifier, substitutionValues)) @@ -87,9 +92,11 @@ class _Query { socket.add(bytes); } - _ParameterValue encodeParameter(_PostgreSQLFormatIdentifier identifier, Map substitutionValues) { + _ParameterValue encodeParameter(_PostgreSQLFormatIdentifier identifier, + Map substitutionValues) { if (identifier.typeCode != null) { - return new _ParameterValue.binary(substitutionValues[identifier.name], identifier.typeCode); + return new _ParameterValue.binary( + substitutionValues[identifier.name], identifier.typeCode); } else { return new _ParameterValue.text(substitutionValues[identifier.name]); } @@ -97,13 +104,16 @@ class _Query { PostgreSQLException validateParameters(List parameterTypeIDs) { var actualParameterTypeCodeIterator = parameterTypeIDs.iterator; - var parametersAreMismatched = specifiedParameterTypeCodes.map((specifiedTypeCode) { + var parametersAreMismatched = + specifiedParameterTypeCodes.map((specifiedTypeCode) { actualParameterTypeCodeIterator.moveNext(); - return actualParameterTypeCodeIterator.current == (specifiedTypeCode ?? actualParameterTypeCodeIterator.current); + return actualParameterTypeCodeIterator.current == + (specifiedTypeCode ?? actualParameterTypeCodeIterator.current); }).any((v) => v == false); if (parametersAreMismatched) { - return new PostgreSQLException("Specified parameter types do not match column parameter types in query ${statement}"); + return new PostgreSQLException( + "Specified parameter types do not match column parameter types in query ${statement}"); } return null; @@ -146,17 +156,21 @@ class _QueryCache { String preparedStatementName; List<_PostgreSQLFormatIdentifier> orderedParameters; List<_FieldDescription> fieldDescriptions; + bool get isValid { - return preparedStatementName != null - && orderedParameters != null - && fieldDescriptions != null; + return preparedStatementName != null && + orderedParameters != null && + fieldDescriptions != null; } } class _ParameterValue { _ParameterValue.binary(dynamic value, this.postgresType) { isBinary = true; - bytes = PostgreSQLCodec.encodeBinary(value, this.postgresType)?.buffer?.asUint8List(); + bytes = PostgreSQLCodec + .encodeBinary(value, this.postgresType) + ?.buffer + ?.asUint8List(); length = bytes?.length ?? 0; } @@ -188,7 +202,8 @@ class _FieldDescription { var buf = new StringBuffer(); var byte = 0; do { - byte = byteData.getUint8(offset); offset += 1; + byte = byteData.getUint8(offset); + offset += 1; if (byte != 0) { buf.writeCharCode(byte); } @@ -196,12 +211,18 @@ class _FieldDescription { fieldName = buf.toString(); - tableID = byteData.getUint32(offset); offset += 4; - columnID = byteData.getUint16(offset); offset += 2; - typeID = byteData.getUint32(offset); offset += 4; - dataTypeSize = byteData.getUint16(offset); offset += 2; - typeModifier = byteData.getInt32(offset); offset += 4; - formatCode = byteData.getUint16(offset); offset += 2; + tableID = byteData.getUint32(offset); + offset += 4; + columnID = byteData.getUint16(offset); + offset += 2; + typeID = byteData.getUint32(offset); + offset += 4; + dataTypeSize = byteData.getUint16(offset); + offset += 2; + typeModifier = byteData.getInt32(offset); + offset += 4; + formatCode = byteData.getUint16(offset); + offset += 2; return offset; } @@ -210,4 +231,3 @@ class _FieldDescription { return "$fieldName $tableID $columnID $typeID $dataTypeSize $typeModifier $formatCode"; } } - diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index 2975855..c0ae051 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -9,7 +9,8 @@ class _ErrorResponseMessage implements _ServerMessage { List<_ErrorField> fields = [new _ErrorField()]; void readBytes(Uint8List bytes) { - var lastByteRemovedList = new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); + var lastByteRemovedList = + new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); lastByteRemovedList.forEach((byte) { if (byte != 0) { @@ -61,7 +62,8 @@ class _ParameterStatusMessage extends _ServerMessage { void readBytes(Uint8List bytes) { name = UTF8.decode(bytes.sublist(0, bytes.indexOf(0))); - value = UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + value = + UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } String toString() => "Parameter Message: $name $value"; @@ -100,7 +102,8 @@ class _RowDescriptionMessage extends _ServerMessage { void readBytes(Uint8List bytes) { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); var offset = 0; - var fieldCount = view.getInt16(offset); offset += 2; + var fieldCount = view.getInt16(offset); + offset += 2; fieldDescriptions = <_FieldDescription>[]; for (var i = 0; i < fieldCount; i++) { @@ -119,17 +122,20 @@ class _DataRowMessage extends _ServerMessage { void readBytes(Uint8List bytes) { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); var offset = 0; - var fieldCount = view.getInt16(offset); offset += 2; + var fieldCount = view.getInt16(offset); + offset += 2; for (var i = 0; i < fieldCount; i++) { - var dataSize = view.getInt32(offset); offset += 4; + var dataSize = view.getInt32(offset); + offset += 4; if (dataSize == 0) { values.add(new ByteData(0)); } else if (dataSize == -1) { values.add(null); } else { - var rawBytes = new ByteData.view(bytes.buffer, bytes.offsetInBytes + offset, dataSize); + var rawBytes = new ByteData.view( + bytes.buffer, bytes.offsetInBytes + offset, dataSize); values.add(rawBytes); offset += dataSize; } @@ -143,6 +149,7 @@ class _CommandCompleteMessage extends _ServerMessage { int rowsAffected; static RegExp identifierExpression = new RegExp(r"[A-Z ]*"); + void readBytes(Uint8List bytes) { var str = UTF8.decode(bytes.sublist(0, bytes.length - 1)); @@ -165,6 +172,7 @@ class _ParseCompleteMessage extends _ServerMessage { class _BindCompleteMessage extends _ServerMessage { void readBytes(Uint8List bytes) {} + String toString() => "Bind Complete Message"; } @@ -175,11 +183,13 @@ class _ParameterDescriptionMessage extends _ServerMessage { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); var offset = 0; - var count = view.getUint16(0); offset += 2; + var count = view.getUint16(0); + offset += 2; parameterTypeIDs = []; for (var i = 0; i < count; i++) { - var v = view.getUint32(offset); offset += 4; + var v = view.getUint32(offset); + offset += 4; parameterTypeIDs.add(v); } } @@ -188,9 +198,7 @@ class _ParameterDescriptionMessage extends _ServerMessage { } class _NoDataMessage extends _ServerMessage { - void readBytes(Uint8List bytes) { - - } + void readBytes(Uint8List bytes) {} String toString() => "No Data Message"; } diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index fc02376..5685b74 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -1,20 +1,21 @@ part of postgres; -typedef String SQLReplaceIdentifierFunction(_PostgreSQLFormatIdentifier identifier, int index); +typedef String SQLReplaceIdentifierFunction( + _PostgreSQLFormatIdentifier identifier, int index); class PostgreSQLFormat { static int _AtSignCodeUnit = "@".codeUnitAt(0); static Map _typeStringToCodeMap = { - "text" : PostgreSQLCodec.TypeText, - "int2" : PostgreSQLCodec.TypeInt2, - "int4" : PostgreSQLCodec.TypeInt4, - "int8" : PostgreSQLCodec.TypeInt8, - "float4" : PostgreSQLCodec.TypeFloat4, - "float8" : PostgreSQLCodec.TypeFloat8, - "boolean" : PostgreSQLCodec.TypeBool, - "date" : PostgreSQLCodec.TypeDate, - "timestamp" : PostgreSQLCodec.TypeTimestamp, - "timestamptz" : PostgreSQLCodec.TypeTimestampTZ + "text": PostgreSQLCodec.TypeText, + "int2": PostgreSQLCodec.TypeInt2, + "int4": PostgreSQLCodec.TypeInt4, + "int8": PostgreSQLCodec.TypeInt8, + "float4": PostgreSQLCodec.TypeFloat4, + "float8": PostgreSQLCodec.TypeFloat8, + "boolean": PostgreSQLCodec.TypeBool, + "date": PostgreSQLCodec.TypeDate, + "timestamp": PostgreSQLCodec.TypeTimestamp, + "timestamptz": PostgreSQLCodec.TypeTimestampTZ }; static String id(String name, {PostgreSQLDataType type: null}) { @@ -27,18 +28,30 @@ class PostgreSQLFormat { static String dataTypeStringForDataType(PostgreSQLDataType dt) { switch (dt) { - case PostgreSQLDataType.text: return "text"; - case PostgreSQLDataType.integer: return "int4"; - case PostgreSQLDataType.smallInteger: return "int2"; - case PostgreSQLDataType.bigInteger: return "int8"; - case PostgreSQLDataType.serial: return "int4"; - case PostgreSQLDataType.bigSerial: return "int8"; - case PostgreSQLDataType.real: return "float4"; - case PostgreSQLDataType.double: return "float8"; - case PostgreSQLDataType.boolean: return "boolean"; - case PostgreSQLDataType.timestampWithoutTimezone: return "timestamp"; - case PostgreSQLDataType.timestampWithTimezone: return "timestamptz"; - case PostgreSQLDataType.date: return "date"; + case PostgreSQLDataType.text: + return "text"; + case PostgreSQLDataType.integer: + return "int4"; + case PostgreSQLDataType.smallInteger: + return "int2"; + case PostgreSQLDataType.bigInteger: + return "int8"; + case PostgreSQLDataType.serial: + return "int4"; + case PostgreSQLDataType.bigSerial: + return "int8"; + case PostgreSQLDataType.real: + return "float4"; + case PostgreSQLDataType.double: + return "float8"; + case PostgreSQLDataType.boolean: + return "boolean"; + case PostgreSQLDataType.timestampWithoutTimezone: + return "timestamp"; + case PostgreSQLDataType.timestampWithTimezone: + return "timestamptz"; + case PostgreSQLDataType.date: + return "date"; } return null; @@ -48,7 +61,8 @@ class PostgreSQLFormat { return _typeStringToCodeMap[dt]; } - static String substitute(String fmtString, Map values, {SQLReplaceIdentifierFunction replace: null}) { + static String substitute(String fmtString, Map values, + {SQLReplaceIdentifierFunction replace: null}) { values ??= {}; replace ??= (spec, index) => PostgreSQLCodec.encode(values[spec.name]); @@ -60,7 +74,8 @@ class PostgreSQLFormat { while (iterator.current != null) { if (lastPtr == null) { if (iterator.current == _AtSignCodeUnit) { - lastPtr = new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); + lastPtr = + new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } else { @@ -70,7 +85,8 @@ class PostgreSQLFormat { } } else if (lastPtr.type == _PostgreSQLFormatTokenType.text) { if (iterator.current == _AtSignCodeUnit) { - lastPtr = new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); + lastPtr = + new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } else { @@ -83,12 +99,12 @@ class PostgreSQLFormat { lastPtr.buffer.writeCharCode(iterator.current); lastPtr.type = _PostgreSQLFormatTokenType.text; } else { - lastPtr = new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); + lastPtr = + new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } iterator.moveNext(); - } else if (_isIdentifier(iterator.current)) { lastPtr.buffer.writeCharCode(iterator.current); } else { @@ -109,11 +125,13 @@ class PostgreSQLFormat { var identifier = new _PostgreSQLFormatIdentifier(t.buffer.toString()); if (!values.containsKey(identifier.name)) { - throw new FormatException("Format string specified identifier with name ${identifier.name}, but key was not present in values. Format string: $fmtString"); + throw new FormatException( + "Format string specified identifier with name ${identifier + .name}, but key was not present in values. Format string: $fmtString"); } var val = replace(identifier, idx); - idx ++; + idx++; return val; } }).join(""); @@ -124,23 +142,21 @@ class PostgreSQLFormat { static int _lowercaseZCodeUnit = "z".codeUnitAt(0); static int _uppercaseZCodeUnit = "Z".codeUnitAt(0); static int _0CodeUnit = "0".codeUnitAt(0); - static int _9CodeUnit= "9".codeUnitAt(0); - static int _underscoreCodeUnit= "_".codeUnitAt(0); + static int _9CodeUnit = "9".codeUnitAt(0); + static int _underscoreCodeUnit = "_".codeUnitAt(0); static int _ColonCodeUnit = ":".codeUnitAt(0); static bool _isIdentifier(int charCode) { - return (charCode >= _lowercaseACodeUnit && charCode <= _lowercaseZCodeUnit) - || (charCode >= _uppercaseACodeUnit && charCode <= _uppercaseZCodeUnit) - || (charCode >= _0CodeUnit && charCode <= _9CodeUnit) - || (charCode == _underscoreCodeUnit) - || (charCode == _ColonCodeUnit); + return (charCode >= _lowercaseACodeUnit && + charCode <= _lowercaseZCodeUnit) || + (charCode >= _uppercaseACodeUnit && charCode <= _uppercaseZCodeUnit) || + (charCode >= _0CodeUnit && charCode <= _9CodeUnit) || + (charCode == _underscoreCodeUnit) || + (charCode == _ColonCodeUnit); } } - -enum _PostgreSQLFormatTokenType { - text, marker -} +enum _PostgreSQLFormatTokenType { text, marker } class _PostgreSQLFormatToken { _PostgreSQLFormatToken(this.type); @@ -159,10 +175,12 @@ class _PostgreSQLFormatIdentifier { var dataTypeString = components.last; if (dataTypeString != null) { - typeCode = PostgreSQLFormat._postgresCodeForDataTypeString(dataTypeString); + typeCode = + PostgreSQLFormat._postgresCodeForDataTypeString(dataTypeString); } } else { - throw new FormatException("Invalid format string identifier, must contain identifier name and optionally one data type in format '@identifier:dataType' (offending identifier: ${t})"); + throw new FormatException( + "Invalid format string identifier, must contain identifier name and optionally one data type in format '@identifier:dataType' (offending identifier: ${t})"); } // Strip @ @@ -171,4 +189,4 @@ class _PostgreSQLFormatIdentifier { String name; int typeCode; -} \ No newline at end of file +} diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index 2880fa3..eed2aa8 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -1,6 +1,7 @@ part of postgres; -typedef Future _TransactionQuerySignature(PostgreSQLExecutionContext connection); +typedef Future _TransactionQuerySignature( + PostgreSQLExecutionContext connection); class _TransactionProxy implements PostgreSQLExecutionContext { _TransactionProxy(this.connection, this.executionBlock) { @@ -14,6 +15,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { _Query beginQuery; Completer completer = new Completer(); + Future get future => completer.future; _Query get pendingQuery { @@ -23,6 +25,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { return null; } + List<_Query> queryQueue = []; PostgreSQLConnection connection; _TransactionQuerySignature executionBlock; @@ -31,12 +34,16 @@ class _TransactionProxy implements PostgreSQLExecutionContext { await execute("COMMIT"); } - Future>> query(String fmtString, {Map substitutionValues: null, bool allowReuse: true}) async { + Future>> query(String fmtString, + {Map substitutionValues: null, + bool allowReuse: true}) async { if (connection.isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } - var query = new _Query>>(fmtString, substitutionValues, connection, this); + var query = new _Query>>( + fmtString, substitutionValues, connection, this); if (allowReuse) { query.statementIdentifier = connection._reuseIdentifierForQuery(query); @@ -45,9 +52,11 @@ class _TransactionProxy implements PostgreSQLExecutionContext { return await enqueue(query); } - Future execute(String fmtString, {Map substitutionValues: null}) async { + Future execute(String fmtString, + {Map substitutionValues: null}) async { if (connection.isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } var query = new _Query(fmtString, substitutionValues, connection, this) @@ -82,9 +91,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { completer.complete(result); } - Future handleTransactionQueryError(dynamic err) async { - - } + Future handleTransactionQueryError(dynamic err) async {} Future enqueue(_Query query) async { queryQueue.add(query); @@ -116,4 +123,4 @@ class PostgreSQLRollback { /// The reason the transaction was cancelled. String reason; -} \ No newline at end of file +} diff --git a/lib/src/utf8_backed_string.dart b/lib/src/utf8_backed_string.dart new file mode 100644 index 0000000..b3ea969 --- /dev/null +++ b/lib/src/utf8_backed_string.dart @@ -0,0 +1,23 @@ +part of postgres; + +class UTF8BackedString { + UTF8BackedString(this.string); + + List _cachedUTF8Bytes; + + final String string; + + int get utf8Length { + if (_cachedUTF8Bytes == null) { + _cachedUTF8Bytes = UTF8.encode(string); + } + return _cachedUTF8Bytes.length; + } + + List get utf8Bytes { + if (_cachedUTF8Bytes == null) { + _cachedUTF8Bytes = UTF8.encode(string); + } + return _cachedUTF8Bytes; + } +} diff --git a/test/connection_test.dart b/test/connection_test.dart index 9e51274..fe273c1 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -13,7 +13,8 @@ void main() { }); test("Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); @@ -21,27 +22,36 @@ void main() { }); test("Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); expect(await conn.execute("select 1"), equals(1)); }); - test("Closing idle connection succeeds, closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test("Closing idle connection succeeds, closes underlying socket", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.close(); - var socketMirror = reflect(conn).type.declarations.values.firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_socket")); + Socket underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee; expect(await underlyingSocket.done, isNotNull); conn = null; }); - test("Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); var futures = [ @@ -67,7 +77,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); }); @@ -75,15 +86,39 @@ void main() { await conn?.close(); }); - test("Issuing multiple queries and awaiting between each one successfully returns the right value", () async { - expect(await conn.query("select 1", allowReuse: false), equals([[1]])); - expect(await conn.query("select 2", allowReuse: false), equals([[2]])); - expect(await conn.query("select 3", allowReuse: false), equals([[3]])); - expect(await conn.query("select 4", allowReuse: false), equals([[4]])); - expect(await conn.query("select 5", allowReuse: false), equals([[5]])); + test( + "Issuing multiple queries and awaiting between each one successfully returns the right value", + () async { + expect( + await conn.query("select 1", allowReuse: false), + equals([ + [1] + ])); + expect( + await conn.query("select 2", allowReuse: false), + equals([ + [2] + ])); + expect( + await conn.query("select 3", allowReuse: false), + equals([ + [3] + ])); + expect( + await conn.query("select 4", allowReuse: false), + equals([ + [4] + ])); + expect( + await conn.query("select 5", allowReuse: false), + equals([ + [5] + ])); }); - test("Issuing multiple queries without awaiting are returned with appropriate values", () async { + test( + "Issuing multiple queries without awaiting are returned with appropriate values", + () async { var futures = [ conn.query("select 1", allowReuse: false), conn.query("select 2", allowReuse: false), @@ -94,7 +129,23 @@ void main() { var results = await Future.wait(futures); - expect(results, [[[1]], [[2]], [[3]], [[4]], [[5]]]); + expect(results, [ + [ + [1] + ], + [ + [2] + ], + [ + [3] + ], + [ + [4] + ], + [ + [5] + ] + ]); }); }); @@ -106,7 +157,8 @@ void main() { }); test("Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); conn.open(); try { @@ -117,8 +169,10 @@ void main() { } }); - test("Starting transaction while opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test("Starting transaction while opening connection triggers error", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); conn.open(); try { @@ -131,8 +185,10 @@ void main() { } }); - test("Invalid password reports error, conn is closed, disables conn", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "notdart"); + test("Invalid password reports error, conn is closed, disables conn", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "notdart"); try { await conn.open(); @@ -144,8 +200,10 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("A query error maintains connectivity, allows future queries", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test("A query error maintains connectivity, allows future queries", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -160,8 +218,11 @@ void main() { await conn.execute("INSERT INTO t (i) VALUES (2)"); }); - test("A query error maintains connectivity, continues processing pending queries", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "A query error maintains connectivity, continues processing pending queries", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -178,17 +239,31 @@ void main() { ]; var results = await Future.wait(futures); - expect(results, [[[1]], [[2]], [[3]]]); - - var queueMirror = reflect(conn).type - .declarations.values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_queryQueue")); - List queue = reflect(conn).getField(queueMirror.simpleName).reflectee; + expect(results, [ + [ + [1] + ], + [ + [2] + ], + [ + [3] + ] + ]); + + var queueMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_queryQueue")); + List queue = + reflect(conn).getField(queueMirror.simpleName).reflectee; expect(queue, isEmpty); }); - test("A query error maintains connectivity, continues processing pending transactions", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "A query error maintains connectivity, continues processing pending transactions", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -207,12 +282,17 @@ void main() { }); orderEnsurer.add(4); - expect(res, [[1]]); + expect(res, [ + [1] + ]); expect(orderEnsurer, [2, 1, 3, 4]); }); - test("Building query throws error, connection continues processing pending queries", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "Building query throws error, connection continues processing pending queries", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); // Make some async queries that'll exit the event loop, but then fail on a query that'll die early @@ -226,15 +306,22 @@ void main() { ]; var results = await Future.wait(futures); - expect(results, [[[1]], [[2]]]); - - var queueMirror = reflect(conn).type - .declarations.values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_queryQueue")); - List queue = reflect(conn).getField(queueMirror.simpleName).reflectee; + expect(results, [ + [ + [1] + ], + [ + [2] + ] + ]); + + var queueMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_queryQueue")); + List queue = + reflect(conn).getField(queueMirror.simpleName).reflectee; expect(queue, isEmpty); }); - }); group("Network error situations", () { @@ -246,7 +333,9 @@ void main() { await socket?.close(); }); - test("Socket fails to connect reports error, disables connection for future use", () async { + test( + "Socket fails to connect reports error, disables connection for future use", + () async { var conn = new PostgreSQLConnection("localhost", 5431, "dart_test"); try { @@ -257,15 +346,19 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("Connection that times out throws appropriate error and cannot be reused", () async { - serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + test( + "Connection that times out throws appropriate error and cannot be reused", + () async { + serverSocket = + await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2); try { await conn.open(); @@ -276,9 +369,11 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("Connection that times out triggers future for pending queries", () async { + test("Connection that times out triggers future for pending queries", + () async { var openCompleter = new Completer(); - serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = + await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -288,7 +383,8 @@ void main() { }); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2); conn.open().catchError((e) {}); await openCompleter.future; @@ -317,4 +413,4 @@ Future expectConnectionIsInvalid(PostgreSQLConnection conn) async { } on PostgreSQLException catch (e) { expect(e.message, contains("Attempting to reopen a closed connection")); } -} \ No newline at end of file +} diff --git a/test/decode_test.dart b/test/decode_test.dart index fa9778f..e990e00 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -4,12 +4,16 @@ import 'package:test/test.dart'; void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES (-2147483648, -9223372036854775808, TRUE, -32768, 'string', 10.0, 10.0, '1983-11-06', '1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES (2147483647, 9223372036854775807, FALSE, 32767, 'a significantly longer string to the point where i doubt this actually matters', 10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', '2183-11-06 00:00:00.999999')"); + await connection.execute( + "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + await connection.execute( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES (-2147483648, -9223372036854775808, TRUE, -32768, 'string', 10.0, 10.0, '1983-11-06', '1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000')"); + await connection.execute( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES (2147483647, 9223372036854775807, FALSE, 32767, 'a significantly longer string to the point where i doubt this actually matters', 10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', '2183-11-06 00:00:00.999999')"); }); tearDown(() async { await connection?.close(); @@ -41,7 +45,10 @@ void main() { expect(row2[3], equals(2)); expect(row2[4], equals(false)); expect(row2[5], equals(32767)); - expect(row2[6], equals("a significantly longer string to the point where i doubt this actually matters")); + expect( + row2[6], + equals( + "a significantly longer string to the point where i doubt this actually matters")); expect(row2[7] is double, true); expect(row2[7], equals(10.25)); expect(row2[8] is double, true); @@ -53,28 +60,33 @@ void main() { test("Fetch/insert empty string", () async { await connection.execute("CREATE TEMPORARY TABLE u (t text)"); - var results = await connection.query("INSERT INTO u (t) VALUES (@t:text) returning t", substitutionValues: { - "t" : "" - }); - expect(results, [[""]]); + var results = await connection.query( + "INSERT INTO u (t) VALUES (@t:text) returning t", + substitutionValues: {"t": ""}); + expect(results, [ + [""] + ]); results = await connection.query("select * from u"); - expect(results, [[""]]); + expect(results, [ + [""] + ]); }); test("Fetch/insert null value", () async { await connection.execute("CREATE TEMPORARY TABLE u (t text)"); - var results = await connection.query("INSERT INTO u (t) VALUES (@t:text) returning t", substitutionValues: { - "t" : null - }); - expect(results, [[null]]); + var results = await connection.query( + "INSERT INTO u (t) VALUES (@t:text) returning t", + substitutionValues: {"t": null}); + expect(results, [ + [null] + ]); results = await connection.query("select * from u"); - expect(results, [[null]]); + expect(results, [ + [null] + ]); }); - - test("Timezone concerns", () { - - }); -} \ No newline at end of file + test("Timezone concerns", () {}); +} diff --git a/test/encoding_test.dart b/test/encoding_test.dart index bc4a586..4c195ed 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -40,37 +40,47 @@ void main() { expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLCodec.TypeTimestamp); expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLCodec.TypeTimestamp); - expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLCodec.TypeTimestampTZ); - expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLCodec.TypeTimestampTZ); + expectInverse( + new DateTime.utc(1920, 10, 1), PostgreSQLCodec.TypeTimestampTZ); + expectInverse( + new DateTime.utc(2120, 10, 5), PostgreSQLCodec.TypeTimestampTZ); }); test("Escape strings", () { // ' b o b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bob')), equals([39, 98, 111, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bob')), + equals([39, 98, 111, 98, 39])); // ' b o \n b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bo\nb')), equals([39, 98, 111, 10, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bo\nb')), + equals([39, 98, 111, 10, 98, 39])); // ' b o \r b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bo\rb')), equals([39, 98, 111, 13, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bo\rb')), + equals([39, 98, 111, 13, 98, 39])); // ' b o \b b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bo\bb')), equals([39, 98, 111, 8, 98, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode('bo\bb')), + equals([39, 98, 111, 8, 98, 39])); // ' ' ' ' expect(UTF8.encode(PostgreSQLCodec.encode("'")), equals([39, 39, 39, 39])); // ' ' ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("''")), equals([39, 39, 39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("''")), + equals([39, 39, 39, 39, 39, 39])); // ' ' ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("\''")), equals([39, 39, 39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("\''")), + equals([39, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("\\''")), + equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); + expect(UTF8.encode(PostgreSQLCodec.encode("\\'")), + equals([32, 69, 39, 92, 92, 39, 39, 39])); }); test("Encode DateTime", () { @@ -81,13 +91,20 @@ void main() { ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; var pairs = { - "2001-02-03T00:00:00.000$tzOffsetDelimiter" : new DateTime(2001, DateTime.FEBRUARY, 3), - "2001-02-03T04:05:06.000$tzOffsetDelimiter" : new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "2001-02-03T04:05:06.999$tzOffsetDelimiter" : new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), - "0010-02-03T04:05:06.123$tzOffsetDelimiter BC" : new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), - "0010-02-03T04:05:06.000$tzOffsetDelimiter BC" : new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter BC" : new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter" : new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0) + "2001-02-03T00:00:00.000$tzOffsetDelimiter": + new DateTime(2001, DateTime.FEBRUARY, 3), + "2001-02-03T04:05:06.000$tzOffsetDelimiter": + new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), + "2001-02-03T04:05:06.999$tzOffsetDelimiter": + new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), + "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": + new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), + "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": + new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": + new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter": + new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0) }; pairs.forEach((k, v) { @@ -97,52 +114,68 @@ void main() { test("Encode Double", () { var pairs = { - "'nan'" : double.NAN, - "'infinity'" : double.INFINITY, - "'-infinity'" : double.NEGATIVE_INFINITY, - "1.7976931348623157e+308" : double.MAX_FINITE, - "5e-324" : double.MIN_POSITIVE, - "-0.0" : -0.0, - "0.0" : 0.0 + "'nan'": double.NAN, + "'infinity'": double.INFINITY, + "'-infinity'": double.NEGATIVE_INFINITY, + "1.7976931348623157e+308": double.MAX_FINITE, + "5e-324": double.MIN_POSITIVE, + "-0.0": -0.0, + "0.0": 0.0 }; pairs.forEach((k, v) { expect(PostgreSQLCodec.encode(v), "$k"); - expect(PostgreSQLCodec.encode(v, dataType: PostgreSQLDataType.real), "$k"); - expect(PostgreSQLCodec.encode(v, dataType: PostgreSQLDataType.double), "$k"); + expect( + PostgreSQLCodec.encode(v, dataType: PostgreSQLDataType.real), "$k"); + expect( + PostgreSQLCodec.encode(v, dataType: PostgreSQLDataType.double), "$k"); }); expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.double), "1"); - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.real), "null"); - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.double), "null"); + expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.real), + "null"); + expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.double), + "null"); }); test("Encode Int", () { - expect(PostgreSQLCodec.encode(1.0, dataType: PostgreSQLDataType.integer), "1"); + expect( + PostgreSQLCodec.encode(1.0, dataType: PostgreSQLDataType.integer), "1"); expect(PostgreSQLCodec.encode(1), "1"); - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.integer), "1"); - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.bigInteger), "1"); - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.smallInteger), "1"); - - - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.integer), "null"); - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.bigInteger), "null"); - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.smallInteger), "null"); + expect( + PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.integer), "1"); + expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.bigInteger), + "1"); + expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.smallInteger), + "1"); + + expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.integer), + "null"); + expect( + PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.bigInteger), + "null"); + expect( + PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.smallInteger), + "null"); }); test("Encode Bool", () { - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.boolean), "null"); + expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.boolean), + "null"); expect(PostgreSQLCodec.encode(true), "TRUE"); expect(PostgreSQLCodec.encode(false), "FALSE"); - expect(PostgreSQLCodec.encode(true, dataType: PostgreSQLDataType.boolean), "TRUE"); - expect(PostgreSQLCodec.encode(false, dataType: PostgreSQLDataType.boolean), "FALSE"); + expect(PostgreSQLCodec.encode(true, dataType: PostgreSQLDataType.boolean), + "TRUE"); + expect(PostgreSQLCodec.encode(false, dataType: PostgreSQLDataType.boolean), + "FALSE"); }); } expectInverse(dynamic value, int dataType) { var encodedValue = PostgreSQLCodec.encodeBinary(value, dataType); - var decodedValue = PostgreSQLCodec.decodeValue(new ByteData.view(encodedValue.buffer), dataType); + var decodedValue = PostgreSQLCodec.decodeValue( + new ByteData.view(encodedValue.buffer), dataType); expect(decodedValue, value); -} \ No newline at end of file +} diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 817faf5..8f74e15 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -4,39 +4,63 @@ import 'package:test/test.dart'; void main() { test("Simple replacement", () { - var result = PostgreSQLFormat.substitute("@id", {"id" : 20}); + var result = PostgreSQLFormat.substitute("@id", {"id": 20}); expect(result, equals("20")); }); test("Trailing/leading space", () { - var result = PostgreSQLFormat.substitute(" @id ", {"id" : 20}); + var result = PostgreSQLFormat.substitute(" @id ", {"id": 20}); expect(result, equals(" 20 ")); }); test("Two identifiers next to eachother", () { - var result = PostgreSQLFormat.substitute("@id@bob", {"id" : 20, "bob" : 13}); + var result = PostgreSQLFormat.substitute("@id@bob", {"id": 20, "bob": 13}); expect(result, equals("2013")); }); test("Identifier with underscores", () { - var result = PostgreSQLFormat.substitute("@_one_two", {"_one_two" : 12}); + var result = PostgreSQLFormat.substitute("@_one_two", {"_one_two": 12}); expect(result, equals("12")); }); test("Identifier with type info", () { - var result = PostgreSQLFormat.substitute("@id:int2", {"id" : 12}); + var result = PostgreSQLFormat.substitute("@id:int2", {"id": 12}); expect(result, equals("12")); }); test("Identifiers next to eachother with type info", () { - var result = PostgreSQLFormat.substitute("@id:int2@foo:float4", {"id" : 12, "foo" : 2.0}); + var result = PostgreSQLFormat + .substitute("@id:int2@foo:float4", {"id": 12, "foo": 2.0}); expect(result, equals("122.0")); }); test("String identifiers get escaped", () { - var result = PostgreSQLFormat.substitute("@id:text @foo", {"id" : "1';select", "foo" : "3\\4"}); + var result = PostgreSQLFormat + .substitute("@id:text @foo", {"id": "1';select", "foo": "3\\4"}); // ' 1 ' ' ; s e l e c t ' sp sp E ' 3 \ \ 4 ' - expect(UTF8.encode(result), [39,49,39,39,59,115,101,108,101,99,116,39, 32, 32,69,39,51,92,92,52,39]); + expect(UTF8.encode(result), [ + 39, + 49, + 39, + 39, + 59, + 115, + 101, + 108, + 101, + 99, + 116, + 39, + 32, + 32, + 69, + 39, + 51, + 92, + 92, + 52, + 39 + ]); }); -} \ No newline at end of file +} diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index f5694f7..8dc928b 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -3,182 +3,276 @@ import 'package:test/test.dart'; import 'dart:async'; import 'dart:mirrors'; -String sid(String id, PostgreSQLDataType dt) => PostgreSQLFormat.id(id, type: dt); +String sid(String id, PostgreSQLDataType dt) => + PostgreSQLFormat.id(id, type: dt); void main() { group("Retaining type information", () { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + await connection.execute( + "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); }); tearDown(() async { await connection.close(); }); - test("Call query multiple times with all parameter types succeeds", () async { - var insertQueryString = "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " + test("Call query multiple times with all parameter types succeeds", + () async { + var insertQueryString = + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " "(${sid("i", PostgreSQLDataType.integer)}, ${sid("bi", PostgreSQLDataType.bigInteger)}," "${sid("bl", PostgreSQLDataType.boolean)}, ${sid("si", PostgreSQLDataType.smallInteger)}," "${sid("t", PostgreSQLDataType.text)}, ${sid("f", PostgreSQLDataType.real)}," "${sid("d", PostgreSQLDataType.double)}, ${sid("dt", PostgreSQLDataType.date)}," "${sid("ts", PostgreSQLDataType.timestampWithoutTimezone)}, ${sid("tsz", PostgreSQLDataType.timestampWithTimezone)}" ") returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz"; - var results = await connection.query(insertQueryString, substitutionValues: { - "i" : 1, - "bi" : 2, - "bl" : true, - "si" : 3, - "t" : "foobar", - "f" : 5.0, - "d" : 6.0, - "dt" : new DateTime.utc(2000), - "ts" : new DateTime.utc(2000, 2), - "tsz" : new DateTime.utc(2000, 3) + var results = + await connection.query(insertQueryString, substitutionValues: { + "i": 1, + "bi": 2, + "bl": true, + "si": 3, + "t": "foobar", + "f": 5.0, + "d": 6.0, + "dt": new DateTime.utc(2000), + "ts": new DateTime.utc(2000, 2), + "tsz": new DateTime.utc(2000, 3) }); expect(hasCachedQueryNamed(connection, insertQueryString), true); - var expectedRow1 = [1, 1, 2, 1, true, 3, "foobar", 5.0, 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3)]; + var expectedRow1 = [ + 1, + 1, + 2, + 1, + true, + 3, + "foobar", + 5.0, + 6.0, + new DateTime.utc(2000), + new DateTime.utc(2000, 2), + new DateTime.utc(2000, 3) + ]; expect(results, [expectedRow1]); results = await connection.query(insertQueryString, substitutionValues: { - "i" : 2, - "bi" : 3, - "bl" : false, - "si" : 4, - "t" : "barfoo", - "f" : 6.0, - "d" : 7.0, - "dt" : new DateTime.utc(2001), - "ts" : new DateTime.utc(2001, 2), - "tsz" : new DateTime.utc(2001, 3) + "i": 2, + "bi": 3, + "bl": false, + "si": 4, + "t": "barfoo", + "f": 6.0, + "d": 7.0, + "dt": new DateTime.utc(2001), + "ts": new DateTime.utc(2001, 2), + "tsz": new DateTime.utc(2001, 3) }); expect(hasCachedQueryNamed(connection, insertQueryString), true); - var expectedRow2 = [2, 2, 3, 2, false, 4, "barfoo", 6.0, 7.0, new DateTime.utc(2001), new DateTime.utc(2001, 2), new DateTime.utc(2001, 3)]; + var expectedRow2 = [ + 2, + 2, + 3, + 2, + false, + 4, + "barfoo", + 6.0, + 7.0, + new DateTime.utc(2001), + new DateTime.utc(2001, 2), + new DateTime.utc(2001, 3) + ]; expect(results, [expectedRow2]); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + results = await connection + .query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); expect(results, [expectedRow1, expectedRow2]); - expect(hasCachedQueryNamed(connection, "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"), true); + expect( + hasCachedQueryNamed(connection, + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"), + true); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", substitutionValues: { - "i" : 0 - }); + results = await connection.query( + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", + substitutionValues: {"i": 0}); expect(results, []); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", substitutionValues: { - "i" : 2 - }); + results = await connection.query( + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", + substitutionValues: {"i": 2}); expect(results, [expectedRow1]); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", substitutionValues: { - "i" : 5 - }); + results = await connection.query( + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", + substitutionValues: {"i": 5}); expect(results, [expectedRow1, expectedRow2]); expect(hasCachedQueryNamed(connection, insertQueryString), true); }); test("Call query multiple times without type data succeeds ", () async { - var insertQueryString = "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " + var insertQueryString = + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " "(@i, @bi, @bl, @si, @t, @f, @d, @dt, @ts, @tsz) " "returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz"; - var results = await connection.query(insertQueryString, substitutionValues: { - "i" : 1, - "bi" : 2, - "bl" : true, - "si" : 3, - "t" : "foobar", - "f" : 5.0, - "d" : 6.0, - "dt" : new DateTime.utc(2000), - "ts" : new DateTime.utc(2000, 2), - "tsz" : new DateTime.utc(2000, 3) + var results = + await connection.query(insertQueryString, substitutionValues: { + "i": 1, + "bi": 2, + "bl": true, + "si": 3, + "t": "foobar", + "f": 5.0, + "d": 6.0, + "dt": new DateTime.utc(2000), + "ts": new DateTime.utc(2000, 2), + "tsz": new DateTime.utc(2000, 3) }); - var expectedRow1 = [1, 1, 2, 1, true, 3, "foobar", 5.0, 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3)]; + var expectedRow1 = [ + 1, + 1, + 2, + 1, + true, + 3, + "foobar", + 5.0, + 6.0, + new DateTime.utc(2000), + new DateTime.utc(2000, 2), + new DateTime.utc(2000, 3) + ]; expect(results, [expectedRow1]); results = await connection.query(insertQueryString, substitutionValues: { - "i" : 2, - "bi" : 3, - "bl" : false, - "si" : 4, - "t" : "barfoo", - "f" : 6.0, - "d" : 7.0, - "dt" : new DateTime.utc(2001), - "ts" : new DateTime.utc(2001, 2), - "tsz" : new DateTime.utc(2001, 3) + "i": 2, + "bi": 3, + "bl": false, + "si": 4, + "t": "barfoo", + "f": 6.0, + "d": 7.0, + "dt": new DateTime.utc(2001), + "ts": new DateTime.utc(2001, 2), + "tsz": new DateTime.utc(2001, 3) }); - var expectedRow2 = [2, 2, 3, 2, false, 4, "barfoo", 6.0, 7.0, new DateTime.utc(2001), new DateTime.utc(2001, 2), new DateTime.utc(2001, 3)]; + var expectedRow2 = [ + 2, + 2, + 3, + 2, + false, + 4, + "barfoo", + 6.0, + 7.0, + new DateTime.utc(2001), + new DateTime.utc(2001, 2), + new DateTime.utc(2001, 3) + ]; expect(results, [expectedRow2]); }); - test("Call query multiple times with partial parameter type info succeeds", () async { - var insertQueryString = "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " + test("Call query multiple times with partial parameter type info succeeds", + () async { + var insertQueryString = + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " "(${sid("i", PostgreSQLDataType.integer)}, @bi," "${sid("bl", PostgreSQLDataType.boolean)}, @si," "${sid("t", PostgreSQLDataType.text)}, @f," "${sid("d", PostgreSQLDataType.double)}, @dt," "${sid("ts", PostgreSQLDataType.timestampWithoutTimezone)}, @tsz" ") returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz"; - var results = await connection.query(insertQueryString, substitutionValues: { - "i" : 1, - "bi" : 2, - "bl" : true, - "si" : 3, - "t" : "foobar", - "f" : 5.0, - "d" : 6.0, - "dt" : new DateTime.utc(2000), - "ts" : new DateTime.utc(2000, 2), - "tsz" : new DateTime.utc(2000, 3) + var results = + await connection.query(insertQueryString, substitutionValues: { + "i": 1, + "bi": 2, + "bl": true, + "si": 3, + "t": "foobar", + "f": 5.0, + "d": 6.0, + "dt": new DateTime.utc(2000), + "ts": new DateTime.utc(2000, 2), + "tsz": new DateTime.utc(2000, 3) }); - var expectedRow1 = [1, 1, 2, 1, true, 3, "foobar", 5.0, 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3)]; + var expectedRow1 = [ + 1, + 1, + 2, + 1, + true, + 3, + "foobar", + 5.0, + 6.0, + new DateTime.utc(2000), + new DateTime.utc(2000, 2), + new DateTime.utc(2000, 3) + ]; expect(results, [expectedRow1]); results = await connection.query(insertQueryString, substitutionValues: { - "i" : 2, - "bi" : 3, - "bl" : false, - "si" : 4, - "t" : "barfoo", - "f" : 6.0, - "d" : 7.0, - "dt" : new DateTime.utc(2001), - "ts" : new DateTime.utc(2001, 2), - "tsz" : new DateTime.utc(2001, 3) + "i": 2, + "bi": 3, + "bl": false, + "si": 4, + "t": "barfoo", + "f": 6.0, + "d": 7.0, + "dt": new DateTime.utc(2001), + "ts": new DateTime.utc(2001, 2), + "tsz": new DateTime.utc(2001, 3) }); - var expectedRow2 = [2, 2, 3, 2, false, 4, "barfoo", 6.0, 7.0, new DateTime.utc(2001), new DateTime.utc(2001, 2), new DateTime.utc(2001, 3)]; + var expectedRow2 = [ + 2, + 2, + 3, + 2, + false, + 4, + "barfoo", + 6.0, + 7.0, + new DateTime.utc(2001), + new DateTime.utc(2001, 2), + new DateTime.utc(2001, 3) + ]; expect(results, [expectedRow2]); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + results = await connection + .query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); expect(results, [expectedRow1, expectedRow2]); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", substitutionValues: { - "i" : 0 - }); + results = await connection.query( + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", + substitutionValues: {"i": 0}); expect(results, []); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", substitutionValues: { - "i" : 2 - }); + results = await connection.query( + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", + substitutionValues: {"i": 2}); expect(results, [expectedRow1]); - results = await connection.query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", substitutionValues: { - "i" : 5 - }); + results = await connection.query( + "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", + substitutionValues: {"i": 5}); expect(results, [expectedRow1, expectedRow2]); }); }); @@ -187,9 +281,11 @@ void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)"); + await connection.execute( + "CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)"); await connection.execute("INSERT INTO t (i1, i2) VALUES (0, 1)"); await connection.execute("INSERT INTO t (i1, i2) VALUES (1, 2)"); await connection.execute("INSERT INTO t (i1, i2) VALUES (2, 3)"); @@ -200,110 +296,160 @@ void main() { await connection.close(); }); - test("Call query multiple times, mixing in unnammed queries, succeeds", () async { - var results = await connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 1 - }); - expect(results, [[2, 3], [3, 4]]); - - results = await connection.query("select i1,i2 from t where i1 > @i1", substitutionValues: { - "i1" : 1 - }, allowReuse: false); - expect(results, [[2, 3], [3, 4]]); - - results = await connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 2 - }); - expect(results, [[3, 4]]); - - results = await connection.query("select i1,i2 from t where i1 > @i1", substitutionValues: { - "i1" : 0 - }, allowReuse: false); - expect(results, [[1, 2], [2, 3], [3, 4]]); - - results = await connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 2 - }); - expect(results, [[3, 4]]); - - expect(hasCachedQueryNamed(connection, "select i1, i2 from t where i1 > @i1"), true); + test("Call query multiple times, mixing in unnammed queries, succeeds", + () async { + var results = await connection.query( + "select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 1}); + expect(results, [ + [2, 3], + [3, 4] + ]); + + results = await connection.query("select i1,i2 from t where i1 > @i1", + substitutionValues: {"i1": 1}, allowReuse: false); + expect(results, [ + [2, 3], + [3, 4] + ]); + + results = await connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 2}); + expect(results, [ + [3, 4] + ]); + + results = await connection.query("select i1,i2 from t where i1 > @i1", + substitutionValues: {"i1": 0}, allowReuse: false); + expect(results, [ + [1, 2], + [2, 3], + [3, 4] + ]); + + results = await connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 2}); + expect(results, [ + [3, 4] + ]); + + expect( + hasCachedQueryNamed( + connection, "select i1, i2 from t where i1 > @i1"), + true); expect(cachedQueryMap(connection).length, 1); }); - test("Call query multiple times, mixing in other named queries, succeeds", () async { - var results = await connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 1 - }); - expect(results, [[2, 3], [3, 4]]); - - results = await connection.query("select i1,i2 from t where i2 < @i2", substitutionValues: { - "i2" : 1 - }); + test("Call query multiple times, mixing in other named queries, succeeds", + () async { + var results = await connection.query( + "select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 1}); + expect(results, [ + [2, 3], + [3, 4] + ]); + + results = await connection.query("select i1,i2 from t where i2 < @i2", + substitutionValues: {"i2": 1}); expect(results, []); - results = await connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 2 - }); - expect(results, [[3, 4]]); - - results = await connection.query("select i1,i2 from t where i2 < @i2", substitutionValues: { - "i2" : 2 - }); - expect(results, [[0, 1]]); - - results = await connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 2 - }); - expect(results, [[3, 4]]); - - expect(hasCachedQueryNamed(connection, "select i1, i2 from t where i1 > @i1"), true); - expect(hasCachedQueryNamed(connection, "select i1,i2 from t where i2 < @i2"), true); + results = await connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 2}); + expect(results, [ + [3, 4] + ]); + + results = await connection.query("select i1,i2 from t where i2 < @i2", + substitutionValues: {"i2": 2}); + expect(results, [ + [0, 1] + ]); + + results = await connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 2}); + expect(results, [ + [3, 4] + ]); + + expect( + hasCachedQueryNamed( + connection, "select i1, i2 from t where i1 > @i1"), + true); + expect( + hasCachedQueryNamed(connection, "select i1,i2 from t where i2 < @i2"), + true); expect(cachedQueryMap(connection).length, 2); }); - test("Call a bunch of named and unnamed queries without awaiting, still process correctly", () async { + test( + "Call a bunch of named and unnamed queries without awaiting, still process correctly", + () async { var futures = [ - connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 1 - }), + connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 1}), connection.execute("select 1"), - connection.query("select i1,i2 from t where i2 < @i2", substitutionValues: { - "i2" : 1 - }), - connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 2 - }), + connection.query("select i1,i2 from t where i2 < @i2", + substitutionValues: {"i2": 1}), + connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 2}), connection.query("select 1", allowReuse: false), - connection.query("select i1,i2 from t where i2 < @i2", substitutionValues: { - "i2" : 2 - }), - connection.query("select i1, i2 from t where i1 > @i1", substitutionValues: { - "i1" : 2 - }) + connection.query("select i1,i2 from t where i2 < @i2", + substitutionValues: {"i2": 2}), + connection.query("select i1, i2 from t where i1 > @i1", + substitutionValues: {"i1": 2}) ]; var results = await Future.wait(futures); - expect(results, [[[2, 3], [3, 4]], 1, [], [[3, 4]], [[1]], [[0, 1]], [[3, 4]]]); + expect(results, [ + [ + [2, 3], + [3, 4] + ], + 1, + [], + [ + [3, 4] + ], + [ + [1] + ], + [ + [0, 1] + ], + [ + [3, 4] + ] + ]); }); test("Make a prepared query that has no parameters", () async { var results = await connection.query("select 1"); - expect(results, [[1]]); + expect(results, [ + [1] + ]); results = await connection.query("select 1"); - expect(results, [[1]]); + expect(results, [ + [1] + ]); }); }); group("Failure cases", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); - await connection.execute("CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); - await connection.execute("CREATE TEMPORARY TABLE n (i1 int, i2 int not null);"); + await connection.execute( + "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + await connection.execute( + "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); + await connection + .execute("CREATE TEMPORARY TABLE n (i1 int, i2 int not null);"); }); tearDown(() async { @@ -319,12 +465,14 @@ void main() { expect(cachedQueryMap(connection).isEmpty, true); }); - test("Trying to parse/describe a query with inaccurate types fails and does not cache query", () async { - var string = "insert into u (i1, i2) values (@i1:text, @i2:text) returning i1, i2"; + test( + "Trying to parse/describe a query with inaccurate types fails and does not cache query", + () async { + var string = + "insert into u (i1, i2) values (@i1:text, @i2:text) returning i1, i2"; try { - await connection.query(string, substitutionValues: { - "i1" : "foo", "i2" : "bar" - }); + await connection + .query(string, substitutionValues: {"i1": "foo", "i2": "bar"}); expect(true, false); } on PostgreSQLException {} @@ -332,12 +480,13 @@ void main() { expect(cachedQueryMap(connection).length, 0); }); - test("A failed bind on initial query fails query, but cached query is available", () async { + test( + "A failed bind on initial query fails query, but cached query is available", + () async { var string = "insert into u (i1, i2) values (@i1, @i2) returning i1, i2"; try { - await connection.query(string, substitutionValues: { - "i1" : "foo", "i2" : "bar" - }); + await connection + .query(string, substitutionValues: {"i1": "foo", "i2": "bar"}); expect(true, false); } on PostgreSQLException {} @@ -347,57 +496,60 @@ void main() { var results = await connection.query("select i1, i2 from u"); expect(results, []); - await connection.query(string, substitutionValues: { - "i1" : 1, - "i2" : 2 - }); + await connection.query(string, substitutionValues: {"i1": 1, "i2": 2}); results = await connection.query("select i1, i2 from u"); - expect(results, [[1, 2]]); + expect(results, [ + [1, 2] + ]); }); - test("Cached query that works the first time, wrong type for params the next time throws early error but can still be used", () async { - await connection.query("insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", substitutionValues: { - "i1" : 1, "i2" : 2 - }); - await connection.query("insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", substitutionValues: { - "i1" : 2, "i2" : 3 - }); + test( + "Cached query that works the first time, wrong type for params the next time throws early error but can still be used", + () async { + await connection.query( + "insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + substitutionValues: {"i1": 1, "i2": 2}); + await connection.query( + "insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + substitutionValues: {"i1": 2, "i2": 3}); var string = "select i1, i2 from u where i1 = @i:int4"; - var results = await connection.query(string, substitutionValues: { - "i" : 1 - }); - expect(results, [[1, 2]]); + var results = + await connection.query(string, substitutionValues: {"i": 1}); + expect(results, [ + [1, 2] + ]); expect(hasCachedQueryNamed(connection, string), true); try { - await connection.query(string, substitutionValues: { - "i" : "foo" - }); + await connection.query(string, substitutionValues: {"i": "foo"}); } on FormatException {} - results = await connection.query(string, substitutionValues: { - "i" : 2 - }); - expect(results, [[2, 3]]); + results = await connection.query(string, substitutionValues: {"i": 2}); + expect(results, [ + [2, 3] + ]); expect(hasCachedQueryNamed(connection, string), true); }); - test("Send two queries that will be the same prepared statement async, first one fails on bind", () async { - await connection.query("insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", substitutionValues: { - "i1" : 1, "i2" : 2 - }, allowReuse: false); + test( + "Send two queries that will be the same prepared statement async, first one fails on bind", + () async { + await connection.query( + "insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + substitutionValues: {"i1": 1, "i2": 2}, + allowReuse: false); var string = "select i1, i2 from u where i1 = @i:int4"; - connection.query(string, substitutionValues: { - "i" : "foo" - }).catchError((e) {}); + connection + .query(string, substitutionValues: {"i": "foo"}).catchError((e) {}); - var results = await connection.query(string, substitutionValues: { - "i" : 1 - }); + var results = + await connection.query(string, substitutionValues: {"i": 1}); - expect(results, [[1, 2]]); + expect(results, [ + [1, 2] + ]); expect(cachedQueryMap(connection).length, 1); expect(hasCachedQueryNamed(connection, string), true); }); @@ -405,12 +557,12 @@ void main() { } Map cachedQueryMap(PostgreSQLConnection connection) { - var reuseMapMirror = reflect(connection).type - .declarations.values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_reuseMap")); - return reflect(connection).getField(reuseMapMirror.simpleName).reflectee as Map; + var reuseMapMirror = reflect(connection).type.declarations.values.firstWhere( + (DeclarationMirror dm) => dm.simpleName.toString().contains("_reuseMap")); + return reflect(connection).getField(reuseMapMirror.simpleName).reflectee + as Map; } bool hasCachedQueryNamed(PostgreSQLConnection connection, String name) { return cachedQueryMap(connection)[name] != null; -} \ No newline at end of file +} diff --git a/test/query_test.dart b/test/query_test.dart index 58f321b..76ef30b 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -3,26 +3,32 @@ import 'package:test/test.dart'; void main() { group("Successful queries", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); - await connection.execute("CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); - await connection.execute("CREATE TEMPORARY TABLE n (i1 int, i2 int not null);"); + await connection.execute( + "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + await connection.execute( + "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); + await connection + .execute("CREATE TEMPORARY TABLE n (i1 int, i2 int not null);"); }); tearDown(() async { await connection.close(); }); - test("UTF8 strings", () async { - var result = await connection.query("INSERT INTO t (t) values " + test("UTF8 strings in value", () async { + var result = await connection.query( + "INSERT INTO t (t) values " "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})" "returning t", substitutionValues: { - "t" : "°∆", + "t": "°∆", }); var expectedRow = ["°∆"]; @@ -32,8 +38,20 @@ void main() { expect(result, [expectedRow]); }); + test("UTF8 strings in query", () async { + var result = + await connection.query("INSERT INTO t (t) values ('°∆') RETURNING t"); + + var expectedRow = ["°∆"]; + expect(result, [expectedRow]); + + result = await connection.query("select t from t"); + expect(result, [expectedRow]); + }); + test("Query without specifying types", () async { - var result = await connection.query("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " + var result = await connection.query( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " "(${PostgreSQLFormat.id("i")}," "${PostgreSQLFormat.id("bi")}," "${PostgreSQLFormat.id("bl")}," @@ -45,26 +63,41 @@ void main() { "${PostgreSQLFormat.id("ts")}," "${PostgreSQLFormat.id("tsz")}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz", substitutionValues: { - "i" : 1, - "bi" : 2, - "bl" : true, - "si" : 3, - "t" : "foobar", - "f" : 5.0, - "d" : 6.0, - "dt" : new DateTime.utc(2000), - "ts" : new DateTime.utc(2000, 2), - "tsz" : new DateTime.utc(2000, 3), + "i": 1, + "bi": 2, + "bl": true, + "si": 3, + "t": "foobar", + "f": 5.0, + "d": 6.0, + "dt": new DateTime.utc(2000), + "ts": new DateTime.utc(2000, 2), + "tsz": new DateTime.utc(2000, 3), }); - var expectedRow = [1, 1, 2, 1, true, 3, "foobar", 5.0, 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3)]; + var expectedRow = [ + 1, + 1, + 2, + 1, + true, + 3, + "foobar", + 5.0, + 6.0, + new DateTime.utc(2000), + new DateTime.utc(2000, 2), + new DateTime.utc(2000, 3) + ]; expect(result, [expectedRow]); - result = await connection.query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + result = await connection + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); expect(result, [expectedRow]); }); test("Query by specifying all types", () async { - var result = await connection.query("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " + var result = await connection.query( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " "(${PostgreSQLFormat.id("i", type: PostgreSQLDataType.integer)}," "${PostgreSQLFormat.id("bi", type: PostgreSQLDataType.bigInteger)}," "${PostgreSQLFormat.id("bl", type: PostgreSQLDataType.boolean)}," @@ -76,27 +109,42 @@ void main() { "${PostgreSQLFormat.id("ts", type: PostgreSQLDataType.timestampWithoutTimezone)}," "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz", substitutionValues: { - "i" : 1, - "bi" : 2, - "bl" : true, - "si" : 3, - "t" : "foobar", - "f" : 5.0, - "d" : 6.0, - "dt" : new DateTime.utc(2000), - "ts" : new DateTime.utc(2000, 2), - "tsz" : new DateTime.utc(2000, 3), + "i": 1, + "bi": 2, + "bl": true, + "si": 3, + "t": "foobar", + "f": 5.0, + "d": 6.0, + "dt": new DateTime.utc(2000), + "ts": new DateTime.utc(2000, 2), + "tsz": new DateTime.utc(2000, 3), }); - var expectedRow = [1, 1, 2, 1, true, 3, "foobar", 5.0, 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3)]; + var expectedRow = [ + 1, + 1, + 2, + 1, + true, + 3, + "foobar", + 5.0, + 6.0, + new DateTime.utc(2000), + new DateTime.utc(2000, 2), + new DateTime.utc(2000, 3) + ]; expect(result, [expectedRow]); - result = await connection.query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + result = await connection + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); expect(result, [expectedRow]); }); test("Query by specifying some types", () async { - var result = await connection.query("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " + var result = await connection.query( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " "(${PostgreSQLFormat.id("i")}," "${PostgreSQLFormat.id("bi", type: PostgreSQLDataType.bigInteger)}," "${PostgreSQLFormat.id("bl")}," @@ -108,81 +156,115 @@ void main() { "${PostgreSQLFormat.id("ts")}," "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz", substitutionValues: { - "i" : 1, - "bi" : 2, - "bl" : true, - "si" : 3, - "t" : "foobar", - "f" : 5.0, - "d" : 6.0, - "dt" : new DateTime.utc(2000), - "ts" : new DateTime.utc(2000, 2), - "tsz" : new DateTime.utc(2000, 3), + "i": 1, + "bi": 2, + "bl": true, + "si": 3, + "t": "foobar", + "f": 5.0, + "d": 6.0, + "dt": new DateTime.utc(2000), + "ts": new DateTime.utc(2000, 2), + "tsz": new DateTime.utc(2000, 3), }); - var expectedRow = [1, 1, 2, 1, true, 3, "foobar", 5.0, 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3)]; + var expectedRow = [ + 1, + 1, + 2, + 1, + true, + 3, + "foobar", + 5.0, + 6.0, + new DateTime.utc(2000), + new DateTime.utc(2000, 2), + new DateTime.utc(2000, 3) + ]; expect(result, [expectedRow]); - result = await connection.query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + result = await connection + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); expect(result, [expectedRow]); }); test("Can supply null for values (binary)", () async { - var results = await connection.query("INSERT INTO n (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", substitutionValues: { - "i1" : null, - "i2" : 1, - }); + var results = await connection.query( + "INSERT INTO n (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + substitutionValues: { + "i1": null, + "i2": 1, + }); - expect(results, [[null, 1]]); + expect(results, [ + [null, 1] + ]); }); test("Can supply null for values (text)", () async { - var results = await connection.query("INSERT INTO n (i1, i2) values (@i1, @i2:int4) returning i1, i2", substitutionValues: { - "i1" : null, - "i2" : 1, - }); + var results = await connection.query( + "INSERT INTO n (i1, i2) values (@i1, @i2:int4) returning i1, i2", + substitutionValues: { + "i1": null, + "i2": 1, + }); - expect(results, [[null, 1]]); + expect(results, [ + [null, 1] + ]); }); test("Overspecifying parameters does not impact query (text)", () async { - var results = await connection.query("INSERT INTO u (i1, i2) values (@i1, @i2) returning i1, i2", substitutionValues: { - "i1" : 0, - "i2" : 1, - "i3" : 0, - }); + var results = await connection.query( + "INSERT INTO u (i1, i2) values (@i1, @i2) returning i1, i2", + substitutionValues: { + "i1": 0, + "i2": 1, + "i3": 0, + }); - expect(results, [[0, 1]]); + expect(results, [ + [0, 1] + ]); }); test("Overspecifying parameters does not impact query (binary)", () async { - var results = await connection.query("INSERT INTO u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", substitutionValues: { - "i1" : 0, - "i2" : 1, - "i3" : 0, - }); + var results = await connection.query( + "INSERT INTO u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + substitutionValues: { + "i1": 0, + "i2": 1, + "i3": 0, + }); - expect(results, [[0, 1]]); + expect(results, [ + [0, 1] + ]); }); }); group("Unsuccesful queries", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)"); + await connection.execute( + "CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)"); }); tearDown(() async { await connection.close(); }); - test("A query that fails on the server will report back an exception through the query method", () async { + test( + "A query that fails on the server will report back an exception through the query method", + () async { try { - await connection.query("INSERT INTO t (i1) values (@i1)", substitutionValues: { - "i1" : 0 - }); + await connection.query("INSERT INTO t (i1) values (@i1)", + substitutionValues: {"i1": 0}); expect(true, false); } on PostgreSQLException catch (e) { expect(e.severity, PostgreSQLSeverity.error); @@ -190,20 +272,25 @@ void main() { } }); - test("Not enough parameters to support format string throws error prior to sending to server", () async { + test( + "Not enough parameters to support format string throws error prior to sending to server", + () async { try { - await connection.query("INSERT INTO t (i1) values (@i1)", substitutionValues: {}); + await connection + .query("INSERT INTO t (i1) values (@i1)", substitutionValues: {}); expect(true, false); } on FormatException catch (e) { - expect(e.message, contains("Format string specified identifier with name i1")); + expect(e.message, + contains("Format string specified identifier with name i1")); } try { await connection.query("INSERT INTO t (i1) values (@i1)"); expect(true, false); } on FormatException catch (e) { - expect(e.message, contains("Format string specified identifier with name i1")); + expect(e.message, + contains("Format string specified identifier with name i1")); } }); }); -} \ No newline at end of file +} diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 841a8db..0f6588f 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -9,7 +9,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -18,19 +19,25 @@ void main() { await conn?.close(); }); - test("Send successful transaction succeeds, returns returned value", () async { + test("Send successful transaction succeeds, returns returned value", + () async { var outResult = await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); return await c.query("SELECT id FROM t"); }); - expect(outResult, [[1]]); + expect(outResult, [ + [1] + ]); var result = await conn.query("SELECT id FROM t"); - expect(result, [[1]]); + expect(result, [ + [1] + ]); }); - test("Query during transaction must wait until transaction is finished", () async { + test("Query during transaction must wait until transaction is finished", + () async { var orderEnsurer = []; var nextCompleter = new Completer.sync(); var outResult = conn.transaction((c) async { @@ -54,11 +61,17 @@ void main() { var firstResult = await outResult; expect(orderEnsurer, [1, 2, 11, 3, 12, 13]); - expect(firstResult, [[1]]); - expect(laterResults, [[1],[2]]); + expect(firstResult, [ + [1] + ]); + expect(laterResults, [ + [1], + [2] + ]); }); - test("Make sure two simultaneous transactions cannot be interwoven", () async { + test("Make sure two simultaneous transactions cannot be interwoven", + () async { var orderEnsurer = []; var firstTransactionFuture = conn.transaction((c) async { @@ -86,8 +99,13 @@ void main() { expect(orderEnsurer, [11, 12, 13, 21, 22, 23]); - expect(firstResults, [[1]]); - expect(secondResults, [[1], [2]]); + expect(firstResults, [ + [1] + ]); + expect(secondResults, [ + [1], + [2] + ]); }); test("May intentionally rollback transaction", () async { @@ -108,7 +126,8 @@ void main() { expect(result, []); }); - test("Intentional rollback from outside of a transaction has no impact", () async { + test("Intentional rollback from outside of a transaction has no impact", + () async { var orderEnsurer = []; var nextCompleter = new Completer.sync(); var outResult = conn.transaction((c) async { @@ -129,7 +148,9 @@ void main() { var results = await outResult; expect(orderEnsurer, [1, 2, 11, 3]); - expect(results, [[1]]); + expect(results, [ + [1] + ]); }); test("A transaction does not preempt pending queries", () async { @@ -142,7 +163,11 @@ void main() { var results = await conn.transaction((ctx) async { return await ctx.query("SELECT id FROM t"); }); - expect(results, [[1], [2], [3]]); + expect(results, [ + [1], + [2], + [3] + ]); }); test("A transaction doesn't have to await on queries", () async { @@ -153,10 +178,16 @@ void main() { }); var total = await conn.query("SELECT id FROM t"); - expect(total, [[1], [2], [3]]); + expect(total, [ + [1], + [2], + [3] + ]); }); - test("A transaction with a rollback and non-await queries rolls back transaction", () async { + test( + "A transaction with a rollback and non-await queries rolls back transaction", + () async { conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)"); ctx.query("INSERT INTO t (id) VALUES (2)"); @@ -176,7 +207,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -190,7 +222,9 @@ void main() { await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); var oneRow = await c.query("SELECT id FROM t"); - expect(oneRow, [[1]]); + expect(oneRow, [ + [1] + ]); // This will error await c.query("INSERT INTO t (id) VALUES (1)"); @@ -251,7 +285,9 @@ void main() { await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); var oneRow = await c.query("SELECT id FROM t"); - expect(oneRow, [[1]]); + expect(oneRow, [ + [1] + ]); // This will error await c.query("INSERT INTO t (id) VALUES (1)"); @@ -270,7 +306,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -350,7 +387,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -427,4 +465,4 @@ void main() { expect(result, []); }); }); -} \ No newline at end of file +} From e2224dadb21510c2273d2f1c1ed93f07612a9571 Mon Sep 17 00:00:00 2001 From: Alex Nachlas Date: Tue, 6 Dec 2016 14:12:56 -0500 Subject: [PATCH 04/73] Updated change log (#8) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdec69d..e2c630c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.4 + +- Fixed issue with buffer length + ## 0.9.3 - Fixed issue with UTF8 encoding From 39dc583dd21dc722fa18464985753d3b012fd5a1 Mon Sep 17 00:00:00 2001 From: Alex Nachlas Date: Tue, 6 Dec 2016 14:15:31 -0500 Subject: [PATCH 05/73] Bumped pubspec --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3294a1e..b9fa1d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: A library to connect and query PostgreSQL databases. -version: 0.9.3 +version: 0.9.4 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: From 82914c2f894a7ef4958a649432978f328a3ce0e8 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 9 Dec 2016 22:03:44 -0500 Subject: [PATCH 06/73] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b9e5c1..af132f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # postgres -[![Build Status](https://travis-ci.org/stablekernel/postgresql-dart.svg?branch=master)](https://travis-ci.org/stablekernel/postgresql-dart) +[![Build Status](https://travis-ci.org/stablekernel/postgresql-dart.svg?branch=master)](https://travis-ci.org/stablekernel/postgresql-dart) [![codecov](https://codecov.io/gh/stablekernel/postgresql-dart/branch/master/graph/badge.svg)](https://codecov.io/gh/stablekernel/postgresql-dart) A library for connecting to and querying PostgreSQL databases. From f78ac6fafb4a2b3c911de891963203112dfbd5a7 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 12 Dec 2016 20:50:13 -0500 Subject: [PATCH 07/73] Fix large query buffer issue and add code coverage (#9) * Added long query tests * Fixin bug in message framer * fixed up import/export/part of * Fixing all tests, improving framer * remove unused log statements * Adding code cov * bump version --- .travis.yml | 3 +- ci/after_script.sh | 7 + ci/script.sh | 7 + lib/postgres.dart | 22 +-- lib/src/client_messages.dart | 75 ++++++----- lib/src/connection.dart | 42 +++--- lib/src/connection_fsm.dart | 72 +++++----- lib/src/constants.dart | 2 - lib/src/exceptions.dart | 104 ++++----------- lib/src/message_window.dart | 179 +++++++++++++++---------- lib/src/postgresql_codec.dart | 4 +- lib/src/query.dart | 134 ++++++++++++++----- lib/src/server_messages.dart | 137 +++++++++++++------ lib/src/substituter.dart | 83 ++---------- lib/src/transaction_proxy.dart | 16 +-- lib/src/utf8_backed_string.dart | 4 +- pubspec.yaml | 3 +- test/encoding_test.dart | 14 +- test/framer_test.dart | 230 ++++++++++++++++++++++++++++++++ test/interpolation_test.dart | 2 +- test/query_test.dart | 128 ++++++++++++++++++ test/transaction_test.dart | 4 +- 22 files changed, 848 insertions(+), 424 deletions(-) create mode 100644 ci/after_script.sh create mode 100644 ci/script.sh create mode 100644 test/framer_test.dart diff --git a/.travis.yml b/.travis.yml index 5e5d158..a09aaa7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,8 @@ before_script: - psql -c 'create user darttrust with createdb;' -U postgres - psql -c 'grant all on database dart_test to darttrust;' -U postgres - pub get -script: pub run test -j 1 -r expanded +script: bash ci/script.sh +after_success: bash ci/after_script.sh branches: only: - master \ No newline at end of file diff --git a/ci/after_script.sh b/ci/after_script.sh new file mode 100644 index 0000000..6bc465c --- /dev/null +++ b/ci/after_script.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [[ "$TRAVIS_BRANCH" == "master" ]]; then + curl -s https://codecov.io/bash > .codecov + chmod +x .codecov + ./.codecov -f lcov.info -X xcode +fi \ No newline at end of file diff --git a/ci/script.sh b/ci/script.sh new file mode 100644 index 0000000..4510e99 --- /dev/null +++ b/ci/script.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +pub run test -j 1 -r expanded +if [[ "$TRAVIS_BRANCH" == "master" ]]; then + pub global activate -sgit https://github.com/stablekernel/codecov_dart.git + dart_codecov_generator --report-on=lib/ --verbose --no-html +fi \ No newline at end of file diff --git a/lib/postgres.dart b/lib/postgres.dart index 5dbe806..ead4103 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -1,21 +1,5 @@ library postgres; -import 'dart:convert'; -import 'dart:io'; -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:crypto/crypto.dart'; - -part 'src/transaction_proxy.dart'; -part 'src/client_messages.dart'; -part 'src/server_messages.dart'; -part 'src/postgresql_codec.dart'; -part 'src/substituter.dart'; -part 'src/connection.dart'; -part 'src/message_window.dart'; -part 'src/connection_fsm.dart'; -part 'src/query.dart'; -part 'src/exceptions.dart'; -part 'src/constants.dart'; -part 'src/utf8_backed_string.dart'; +export 'src/connection.dart'; +export 'src/postgresql_codec.dart'; +export 'src/substituter.dart'; diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 77e23d1..9b368ab 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -1,6 +1,10 @@ -part of postgres; +import 'utf8_backed_string.dart'; +import 'dart:typed_data'; +import 'query.dart'; +import 'constants.dart'; +import 'package:crypto/crypto.dart'; -abstract class _ClientMessage { +abstract class ClientMessage { static const int FormatText = 0; static const int FormatBinary = 1; @@ -44,9 +48,9 @@ abstract class _ClientMessage { return buffer.buffer.asUint8List(); } - static Uint8List aggregateBytes(List<_ClientMessage> messages) { + static Uint8List aggregateBytes(List messages) { var totalLength = - messages.fold(0, (total, _ClientMessage next) => total + next.length); + messages.fold(0, (total, ClientMessage next) => total + next.length); var buffer = new ByteData(totalLength); var offset = 0; @@ -57,8 +61,8 @@ abstract class _ClientMessage { } } -class _StartupMessage extends _ClientMessage { - _StartupMessage(String databaseName, String timeZone, {String username}) { +class StartupMessage extends ClientMessage { + StartupMessage(String databaseName, String timeZone, {String username}) { this.databaseName = new UTF8BackedString(databaseName); this.timeZone = new UTF8BackedString(timeZone); if (username != null) { @@ -85,7 +89,7 @@ class _StartupMessage extends _ClientMessage { int applyToBuffer(ByteData buffer, int offset) { buffer.setInt32(offset, length); offset += 4; - buffer.setInt32(offset, _ClientMessage.ProtocolVersion); + buffer.setInt32(offset, ClientMessage.ProtocolVersion); offset += 4; if (username != null) { @@ -110,8 +114,8 @@ class _StartupMessage extends _ClientMessage { } } -class _AuthMD5Message extends _ClientMessage { - _AuthMD5Message(String username, String password, List saltBytes) { +class AuthMD5Message extends ClientMessage { + AuthMD5Message(String username, String password, List saltBytes) { var passwordHash = md5.convert("${password}${username}".codeUnits).toString(); var saltString = new String.fromCharCodes(saltBytes); @@ -126,7 +130,7 @@ class _AuthMD5Message extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.PasswordIdentifier); + buffer.setUint8(offset, ClientMessage.PasswordIdentifier); offset += 1; buffer.setUint32(offset, length - 1); offset += 4; @@ -136,8 +140,8 @@ class _AuthMD5Message extends _ClientMessage { } } -class _QueryMessage extends _ClientMessage { - _QueryMessage(String queryString) { +class QueryMessage extends ClientMessage { + QueryMessage(String queryString) { this.queryString = new UTF8BackedString(queryString); } @@ -148,7 +152,7 @@ class _QueryMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.QueryIdentifier); + buffer.setUint8(offset, ClientMessage.QueryIdentifier); offset += 1; buffer.setUint32(offset, length - 1); offset += 4; @@ -158,8 +162,8 @@ class _QueryMessage extends _ClientMessage { } } -class _ParseMessage extends _ClientMessage { - _ParseMessage(String statement, {String statementName: ""}) { +class ParseMessage extends ClientMessage { + ParseMessage(String statement, {String statementName: ""}) { this.statement = new UTF8BackedString(statement); this.statementName = new UTF8BackedString(statementName); } @@ -172,7 +176,7 @@ class _ParseMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.ParseIdentifier); + buffer.setUint8(offset, ClientMessage.ParseIdentifier); offset += 1; buffer.setUint32(offset, length - 1); offset += 4; @@ -187,8 +191,8 @@ class _ParseMessage extends _ClientMessage { } } -class _DescribeMessage extends _ClientMessage { - _DescribeMessage({String statementName: ""}) { +class DescribeMessage extends ClientMessage { + DescribeMessage({String statementName: ""}) { this.statementName = new UTF8BackedString(statementName); } @@ -199,7 +203,7 @@ class _DescribeMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.DescribeIdentifier); + buffer.setUint8(offset, ClientMessage.DescribeIdentifier); offset += 1; buffer.setUint32(offset, length - 1); offset += 4; @@ -212,13 +216,13 @@ class _DescribeMessage extends _ClientMessage { } } -class _BindMessage extends _ClientMessage { - _BindMessage(this.parameters, {String statementName: ""}) { +class BindMessage extends ClientMessage { + BindMessage(this.parameters, {String statementName: ""}) { typeSpecCount = parameters.where((p) => p.isBinary).length; this.statementName = new UTF8BackedString(statementName); } - List<_ParameterValue> parameters; + List parameters; UTF8BackedString statementName; int typeSpecCount; @@ -234,7 +238,7 @@ class _BindMessage extends _ClientMessage { _cachedLength = 15; _cachedLength += statementName.utf8Length; _cachedLength += inputParameterElementCount * 2; - _cachedLength += parameters.fold(0, (len, _ParameterValue paramValue) { + _cachedLength += parameters.fold(0, (len, ParameterValue paramValue) { if (paramValue.bytes == null) { return len + 4; } else { @@ -246,7 +250,7 @@ class _BindMessage extends _ClientMessage { } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.BindIdentifier); + buffer.setUint8(offset, ClientMessage.BindIdentifier); offset += 1; buffer.setUint32(offset, length - 1); offset += 4; @@ -262,24 +266,21 @@ class _BindMessage extends _ClientMessage { buffer.setUint16(offset, 1); // Apply following format code for all parameters by indicating 1 offset += 2; - buffer.setUint16(offset, _ClientMessage.FormatBinary); + buffer.setUint16(offset, ClientMessage.FormatBinary); offset += 2; // Specify format code for all params is BINARY } else if (typeSpecCount == 0) { buffer.setUint16(offset, 1); // Apply following format code for all parameters by indicating 1 offset += 2; - buffer.setUint16(offset, _ClientMessage.FormatText); + buffer.setUint16(offset, ClientMessage.FormatText); offset += 2; // Specify format code for all params is TEXT } else { // Well, we have some text and some binary, so we have to be explicit about each one buffer.setUint16(offset, parameters.length); offset += 2; parameters.forEach((p) { - buffer.setUint16( - offset, - p.isBinary - ? _ClientMessage.FormatBinary - : _ClientMessage.FormatText); + buffer.setUint16(offset, + p.isBinary ? ClientMessage.FormatBinary : ClientMessage.FormatText); offset += 2; }); } @@ -311,15 +312,15 @@ class _BindMessage extends _ClientMessage { } } -class _ExecuteMessage extends _ClientMessage { - _ExecuteMessage(); +class ExecuteMessage extends ClientMessage { + ExecuteMessage(); int get length { return 10; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.ExecuteIdentifier); + buffer.setUint8(offset, ClientMessage.ExecuteIdentifier); offset += 1; buffer.setUint32(offset, length - 1); offset += 4; @@ -331,15 +332,15 @@ class _ExecuteMessage extends _ClientMessage { } } -class _SyncMessage extends _ClientMessage { - _SyncMessage(); +class SyncMessage extends ClientMessage { + SyncMessage(); int get length { return 5; } int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, _ClientMessage.SyncIdentifier); + buffer.setUint8(offset, ClientMessage.SyncIdentifier); offset += 1; buffer.setUint32(offset, 4); offset += 4; diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 0494849..d6d9246 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -1,4 +1,16 @@ -part of postgres; +library postgres.connection; + +import 'dart:async'; +import 'message_window.dart'; +import 'query.dart'; + +import 'server_messages.dart'; +import 'dart:io'; +import 'client_messages.dart'; + +part 'connection_fsm.dart'; +part 'transaction_proxy.dart'; +part 'exceptions.dart'; abstract class PostgreSQLExecutionContext { /// Executes a query on this context. @@ -105,9 +117,9 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { Map settings = {}; Socket _socket; - _MessageFramer _framer = new _MessageFramer(); + MessageFramer _framer = new MessageFramer(); - Map _reuseMap = {}; + Map _reuseMap = {}; int _reuseCounter = 0; int _processID; @@ -117,9 +129,9 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { bool _hasConnectedPreviously = false; _PostgreSQLConnectionState _connectionState; - List<_Query> _queryQueue = []; + List _queryQueue = []; - _Query get _pendingQuery { + Query get _pendingQuery { if (_queryQueue.isEmpty) { return null; } @@ -151,7 +163,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { .timeout(new Duration(seconds: timeoutInSeconds)); } - _framer = new _MessageFramer(); + _framer = new MessageFramer(); _socket.listen(_readData, onError: _handleSocketError, onDone: _handleSocketClosed); @@ -210,7 +222,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { "Attempting to execute query, but connection is not open."); } - var query = new _Query>>( + var query = new Query>>( fmtString, substitutionValues, this, null); if (allowReuse) { query.statementIdentifier = _reuseIdentifierForQuery(query); @@ -233,7 +245,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { "Attempting to execute query, but connection is not open."); } - var query = new _Query(fmtString, substitutionValues, this, null) + var query = new Query(fmtString, substitutionValues, this, null) ..onlyReturnAffectedRowCount = true; return await _enqueue(query); @@ -286,7 +298,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { //////// - Future _enqueue(_Query query) async { + Future _enqueue(Query query) async { _queryQueue.add(query); _transitionToState(_connectionState.awake()); @@ -342,17 +354,15 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { // as soon as a close occurs, we detach the data stream from anything that actually does // anything with that data. _framer.addBytes(bytes); - while (_framer.hasMessage) { var msg = _framer.popMessage().message; - try { - if (msg is _ErrorResponseMessage) { + if (msg is ErrorResponseMessage) { _transitionToState(_connectionState.onErrorResponse(msg)); } else { _transitionToState(_connectionState.onMessage(msg)); } - } catch (e, st) { + } catch (e) { _handleSocketError(e); } } @@ -371,7 +381,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _cancelCurrentQueries(); } - void _cacheQuery(_Query query) { + void _cacheQuery(Query query) { if (query.cache == null) { return; } @@ -381,7 +391,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { } } - _QueryCache _cachedQuery(String statementIdentifier) { + QueryCache _cachedQuery(String statementIdentifier) { if (statementIdentifier == null) { return null; } @@ -389,7 +399,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { return _reuseMap[statementIdentifier]; } - String _reuseIdentifierForQuery(_Query q) { + String _reuseIdentifierForQuery(Query q) { var existing = _reuseMap[q.statement]; if (existing != null) { return existing.preparedStatementName; diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 857c6e4..b37949c 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -1,4 +1,4 @@ -part of postgres; +part of postgres.connection; abstract class _PostgreSQLConnectionState { PostgreSQLConnection connection; @@ -11,11 +11,11 @@ abstract class _PostgreSQLConnectionState { return this; } - _PostgreSQLConnectionState onMessage(_ServerMessage message) { + _PostgreSQLConnectionState onMessage(ServerMessage message) { return this; } - _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { + _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); if (exception.severity == PostgreSQLSeverity.fatal || @@ -46,7 +46,7 @@ class _PostgreSQLConnectionStateSocketConnected Completer completer; _PostgreSQLConnectionState onEnter() { - var startupMessage = new _StartupMessage( + var startupMessage = new StartupMessage( connection.databaseName, connection.timeZone, username: connection.username); @@ -55,7 +55,7 @@ class _PostgreSQLConnectionStateSocketConnected return this; } - _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { + _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); completer.completeError(exception); @@ -63,13 +63,13 @@ class _PostgreSQLConnectionStateSocketConnected return new _PostgreSQLConnectionStateClosed(); } - _PostgreSQLConnectionState onMessage(_ServerMessage message) { - _AuthenticationMessage authMessage = message; + _PostgreSQLConnectionState onMessage(ServerMessage message) { + AuthenticationMessage authMessage = message; // Pass on the pending op to subsequent stages - if (authMessage.type == _AuthenticationMessage.KindOK) { + if (authMessage.type == AuthenticationMessage.KindOK) { return new _PostgreSQLConnectionStateAuthenticated(completer); - } else if (authMessage.type == _AuthenticationMessage.KindMD5Password) { + } else if (authMessage.type == AuthenticationMessage.KindMD5Password) { connection._salt = authMessage.salt; return new _PostgreSQLConnectionStateAuthenticating(completer); @@ -94,7 +94,7 @@ class _PostgreSQLConnectionStateAuthenticating Completer completer; _PostgreSQLConnectionState onEnter() { - var authMessage = new _AuthMD5Message( + var authMessage = new AuthMD5Message( connection.username, connection.password, connection._salt); connection._socket.add(authMessage.asBytes()); @@ -102,7 +102,7 @@ class _PostgreSQLConnectionStateAuthenticating return this; } - _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { + _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); completer.completeError(exception); @@ -110,14 +110,14 @@ class _PostgreSQLConnectionStateAuthenticating return new _PostgreSQLConnectionStateClosed(); } - _PostgreSQLConnectionState onMessage(_ServerMessage message) { - if (message is _ParameterStatusMessage) { + _PostgreSQLConnectionState onMessage(ServerMessage message) { + if (message is ParameterStatusMessage) { connection.settings[message.name] = message.value; - } else if (message is _BackendKeyMessage) { + } else if (message is BackendKeyMessage) { connection._secretKey = message.secretKey; connection._processID = message.processID; - } else if (message is _ReadyForQueryMessage) { - if (message.state == _ReadyForQueryMessage.StateIdle) { + } else if (message is ReadyForQueryMessage) { + if (message.state == ReadyForQueryMessage.StateIdle) { return new _PostgreSQLConnectionStateIdle(openCompleter: completer); } } @@ -136,7 +136,7 @@ class _PostgreSQLConnectionStateAuthenticated Completer completer; - _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { + _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); completer.completeError(exception); @@ -144,14 +144,14 @@ class _PostgreSQLConnectionStateAuthenticated return new _PostgreSQLConnectionStateClosed(); } - _PostgreSQLConnectionState onMessage(_ServerMessage message) { - if (message is _ParameterStatusMessage) { + _PostgreSQLConnectionState onMessage(ServerMessage message) { + if (message is ParameterStatusMessage) { connection.settings[message.name] = message.value; - } else if (message is _BackendKeyMessage) { + } else if (message is BackendKeyMessage) { connection._secretKey = message.secretKey; connection._processID = message.processID; - } else if (message is _ReadyForQueryMessage) { - if (message.state == _ReadyForQueryMessage.StateIdle) { + } else if (message is ReadyForQueryMessage) { + if (message.state == ReadyForQueryMessage.StateIdle) { return new _PostgreSQLConnectionStateIdle(openCompleter: completer); } } @@ -178,7 +178,7 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { return this; } - _PostgreSQLConnectionState processQuery(_Query q) { + _PostgreSQLConnectionState processQuery(Query q) { try { if (q.onlyReturnAffectedRowCount) { q.sendSimple(connection._socket); @@ -205,7 +205,7 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { return awake(); } - _PostgreSQLConnectionState onMessage(_ServerMessage message) { + _PostgreSQLConnectionState onMessage(ServerMessage message) { return this; } } @@ -217,11 +217,11 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateBusy(this.query); - _Query query; + Query query; PostgreSQLException returningException = null; int rowsAffected = 0; - _PostgreSQLConnectionState onErrorResponse(_ErrorResponseMessage message) { + _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { // If we get an error here, then we should eat the rest of the messages // and we are always confirmed to get a _ReadyForQueryMessage to finish up. // We should only report the error once that is done. @@ -236,14 +236,14 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { return this; } - _PostgreSQLConnectionState onMessage(_ServerMessage message) { + _PostgreSQLConnectionState onMessage(ServerMessage message) { // We ignore NoData, as it doesn't tell us anything we don't already know // or care about. //print("(${query.statement}) -> $message"); - if (message is _ReadyForQueryMessage) { - if (message.state == _ReadyForQueryMessage.StateIdle) { + if (message is ReadyForQueryMessage) { + if (message.state == ReadyForQueryMessage.StateIdle) { if (returningException != null) { query.completeError(returningException); } else { @@ -251,7 +251,7 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } return new _PostgreSQLConnectionStateIdle(); - } else if (message.state == _ReadyForQueryMessage.StateTransaction) { + } else if (message.state == ReadyForQueryMessage.StateTransaction) { if (returningException != null) { query.completeError(returningException); } else { @@ -260,19 +260,19 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { return new _PostgreSQLConnectionStateReadyInTransaction( query.transaction); - } else if (message.state == _ReadyForQueryMessage.StateTransactionError) { + } else if (message.state == ReadyForQueryMessage.StateTransactionError) { // This should cancel the transaction, we may have to send a commit here query.completeError(returningException); return new _PostgreSQLConnectionStateTransactionFailure( query.transaction); } - } else if (message is _CommandCompleteMessage) { + } else if (message is CommandCompleteMessage) { rowsAffected = message.rowsAffected; - } else if (message is _RowDescriptionMessage) { + } else if (message is RowDescriptionMessage) { query.fieldDescriptions = message.fieldDescriptions; - } else if (message is _DataRowMessage) { + } else if (message is DataRowMessage) { query.addRow(message.values); - } else if (message is _ParameterDescriptionMessage) { + } else if (message is ParameterDescriptionMessage) { var validationException = query.validateParameters(message.parameterTypeIDs); if (validationException != null) { @@ -306,7 +306,7 @@ class _PostgreSQLConnectionStateReadyInTransaction return this; } - _PostgreSQLConnectionState processQuery(_Query q) { + _PostgreSQLConnectionState processQuery(Query q) { try { if (q.onlyReturnAffectedRowCount) { q.sendSimple(connection._socket); diff --git a/lib/src/constants.dart b/lib/src/constants.dart index bb3533c..1364c63 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,5 +1,3 @@ -part of postgres; - class UTF8ByteConstants { static const user = const [117, 115, 101, 114, 0]; static const database = const [100, 97, 116, 97, 98, 97, 115, 101, 0]; diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index 0e1b5bb..09bdd60 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -1,4 +1,4 @@ -part of postgres; +part of postgres.connection; /// The severity level of a [PostgreSQLException]. /// @@ -42,35 +42,35 @@ class PostgreSQLException implements Exception { code = ""; } - PostgreSQLException._(List<_ErrorField> errorFields, {this.stackTrace}) { + PostgreSQLException._(List errorFields, {this.stackTrace}) { var finder = (int identifer) => (errorFields.firstWhere( - (_ErrorField e) => e.identificationToken == identifer, + (ErrorField e) => e.identificationToken == identifer, orElse: () => null)); - severity = _ErrorField - .severityFromString(finder(_ErrorField.SeverityIdentifier).text); - code = finder(_ErrorField.CodeIdentifier).text; - message = finder(_ErrorField.MessageIdentifier).text; - detail = finder(_ErrorField.DetailIdentifier)?.text; - hint = finder(_ErrorField.HintIdentifier)?.text; - - internalQuery = finder(_ErrorField.InternalQueryIdentifier)?.text; - trace = finder(_ErrorField.WhereIdentifier)?.text; - schemaName = finder(_ErrorField.SchemaIdentifier)?.text; - tableName = finder(_ErrorField.TableIdentifier)?.text; - columnName = finder(_ErrorField.ColumnIdentifier)?.text; - dataTypeName = finder(_ErrorField.DataTypeIdentifier)?.text; - constraintName = finder(_ErrorField.ConstraintIdentifier)?.text; - fileName = finder(_ErrorField.FileIdentifier)?.text; - routineName = finder(_ErrorField.RoutineIdentifier)?.text; - - var i = finder(_ErrorField.PositionIdentifier)?.text; + severity = ErrorField + .severityFromString(finder(ErrorField.SeverityIdentifier).text); + code = finder(ErrorField.CodeIdentifier).text; + message = finder(ErrorField.MessageIdentifier).text; + detail = finder(ErrorField.DetailIdentifier)?.text; + hint = finder(ErrorField.HintIdentifier)?.text; + + internalQuery = finder(ErrorField.InternalQueryIdentifier)?.text; + trace = finder(ErrorField.WhereIdentifier)?.text; + schemaName = finder(ErrorField.SchemaIdentifier)?.text; + tableName = finder(ErrorField.TableIdentifier)?.text; + columnName = finder(ErrorField.ColumnIdentifier)?.text; + dataTypeName = finder(ErrorField.DataTypeIdentifier)?.text; + constraintName = finder(ErrorField.ConstraintIdentifier)?.text; + fileName = finder(ErrorField.FileIdentifier)?.text; + routineName = finder(ErrorField.RoutineIdentifier)?.text; + + var i = finder(ErrorField.PositionIdentifier)?.text; position = (i != null ? int.parse(i) : null); - i = finder(_ErrorField.InternalPositionIdentifier)?.text; + i = finder(ErrorField.InternalPositionIdentifier)?.text; internalPosition = (i != null ? int.parse(i) : null); - i = finder(_ErrorField.LineIdentifier)?.text; + i = finder(ErrorField.LineIdentifier)?.text; lineNumber = (i != null ? int.parse(i) : null); } @@ -114,61 +114,3 @@ class PostgreSQLException implements Exception { String toString() => "$severity $code: $message Detail: $detail Hint: $hint Table: $tableName Column: $columnName Constraint: $constraintName"; } - -class _ErrorField { - static const int SeverityIdentifier = 83; - static const int CodeIdentifier = 67; - static const int MessageIdentifier = 77; - static const int DetailIdentifier = 68; - static const int HintIdentifier = 72; - static const int PositionIdentifier = 80; - static const int InternalPositionIdentifier = 112; - static const int InternalQueryIdentifier = 113; - static const int WhereIdentifier = 87; - static const int SchemaIdentifier = 115; - static const int TableIdentifier = 116; - static const int ColumnIdentifier = 99; - static const int DataTypeIdentifier = 100; - static const int ConstraintIdentifier = 110; - static const int FileIdentifier = 70; - static const int LineIdentifier = 76; - static const int RoutineIdentifier = 82; - - static PostgreSQLSeverity severityFromString(String str) { - switch (str) { - case "ERROR": - return PostgreSQLSeverity.error; - case "FATAL": - return PostgreSQLSeverity.fatal; - case "PANIC": - return PostgreSQLSeverity.panic; - case "WARNING": - return PostgreSQLSeverity.warning; - case "NOTICE": - return PostgreSQLSeverity.notice; - case "DEBUG": - return PostgreSQLSeverity.debug; - case "INFO": - return PostgreSQLSeverity.info; - case "LOG": - return PostgreSQLSeverity.log; - } - - return PostgreSQLSeverity.unknown; - } - - int identificationToken; - - String get text => _buffer.toString(); - StringBuffer _buffer = new StringBuffer(); - - void add(int byte) { - if (identificationToken == null) { - identificationToken = byte; - } else { - _buffer.writeCharCode(byte); - } - } - - String toString() => text; -} diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 45691f2..a1c3663 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -1,108 +1,147 @@ -part of postgres; - -class _MessageFrame { - static Map _messageTypeMap = { - 49: () => new _ParseCompleteMessage(), - 50: () => new _BindCompleteMessage(), - 67: () => new _CommandCompleteMessage(), - 68: () => new _DataRowMessage(), - 69: () => new _ErrorResponseMessage(), - 75: () => new _BackendKeyMessage(), - 82: () => new _AuthenticationMessage(), - 83: () => new _ParameterStatusMessage(), - 84: () => new _RowDescriptionMessage(), - 90: () => new _ReadyForQueryMessage(), - 110: () => new _NoDataMessage(), - 116: () => new _ParameterDescriptionMessage() +import 'dart:typed_data'; +import 'dart:io'; +import 'server_messages.dart'; +import 'dart:math'; + +class MessageFrame { + static const int HeaderByteSize = 5; + static Map messageTypeMap = { + 49: () => new ParseCompleteMessage(), + 50: () => new BindCompleteMessage(), + 67: () => new CommandCompleteMessage(), + 68: () => new DataRowMessage(), + 69: () => new ErrorResponseMessage(), + 75: () => new BackendKeyMessage(), + 82: () => new AuthenticationMessage(), + 83: () => new ParameterStatusMessage(), + 84: () => new RowDescriptionMessage(), + 90: () => new ReadyForQueryMessage(), + 110: () => new NoDataMessage(), + 116: () => new ParameterDescriptionMessage() }; - BytesBuilder _inputBuffer = new BytesBuilder(copy: false); + int get bytesAvailable => packets.fold(0, (sum, v) => sum + v.lengthInBytes); + List packets = []; + bool get hasReadHeader => type != null; int type; int expectedLength; - bool get isComplete => data != null; + bool get isComplete => data != null || expectedLength == 0; Uint8List data; - int addBytes(Uint8List bytes) { - // If we just have the beginning of a packet, then consume the bytes and continue. - if (_inputBuffer.length + bytes.length < 5) { - _inputBuffer.add(bytes); - return bytes.length; + ByteData consumeNextBytes(int length) { + if (length == 0) { + return null; } - // If we have enough data to get the header out, peek at that data and store it - // This could be 5 if we haven't collected any data yet, or 1-4 if got a few bytes - // from a previous packet. It can't be <= 0 though, as the first precondition - // would have failed and we'd be right here. - var countNeededFromIncomingToDetermineMessage = 5 - _inputBuffer.length; - var headerBuffer = new Uint8List(5); - if (countNeededFromIncomingToDetermineMessage < 5) { - var takenBytes = _inputBuffer.takeBytes(); - headerBuffer.setRange(0, takenBytes.length, takenBytes); - } - headerBuffer.setRange( - 5 - countNeededFromIncomingToDetermineMessage, - 5, - new Uint8List.view(bytes.buffer, bytes.offsetInBytes, - countNeededFromIncomingToDetermineMessage)); - - var bufReader = new ByteData.view(headerBuffer.buffer); - type = bufReader.getUint8(0); - // Remove this length from the length needed to complete this message - expectedLength = bufReader.getUint32(1) - 4; - - var offsetIntoIncomingBytes = countNeededFromIncomingToDetermineMessage; - var byteBufferLengthRemaining = bytes.length - offsetIntoIncomingBytes; - if (byteBufferLengthRemaining >= expectedLength) { - _inputBuffer.add(new Uint8List.view(bytes.buffer, - bytes.offsetInBytes + offsetIntoIncomingBytes, expectedLength)); - data = _inputBuffer.takeBytes(); - return offsetIntoIncomingBytes + expectedLength; + if (bytesAvailable >= length) { + var firstPacket = packets.first; + + // The packet exactly matches the size of the bytes needed, + // remove & return it. + if (firstPacket.lengthInBytes == length) { + packets.removeAt(0); + return firstPacket.buffer.asByteData(firstPacket.offsetInBytes, firstPacket.lengthInBytes); + } + + if (firstPacket.lengthInBytes > length) { + // We have to split up this packet and remove & return the first portion of it, + // and replace it with the second portion of it. + var remainingOffset = firstPacket.offsetInBytes + length; + var bytesNeeded = firstPacket.buffer.asByteData(firstPacket.offsetInBytes, length); + var bytesRemaining = firstPacket.buffer.asUint8List(remainingOffset, firstPacket.lengthInBytes - length); + packets.removeAt(0); + packets.insert(0, bytesRemaining); + + return bytesNeeded; + } + + // Otherwise, the first packet can't fill this message, but we know + // we have enough packets overall to fulfill it. So we can build + // a total buffer by accumulating multiple packets into that buffer. + // Each packet gets removed along the way, except for the last one, + // in which case if it has more bytes available, it gets replaced + // with the remaining bytes. + + var builder = new BytesBuilder(copy: false); + var bytesNeeded = length - builder.length; + while(bytesNeeded > 0) { + var packet = packets.removeAt(0); + var bytesRemaining = packet.lengthInBytes; + + if (bytesRemaining <= bytesNeeded) { + builder.add(packet.buffer.asUint8List(packet.offsetInBytes, packet.lengthInBytes)); + } else { + builder.add(packet.buffer.asUint8List(packet.offsetInBytes, bytesNeeded)); + packets.insert(0, packet.buffer.asUint8List(bytesNeeded, bytesRemaining - bytesNeeded)); + } + + bytesNeeded = length - builder.length; + } + + return new Uint8List.fromList(builder.takeBytes()).buffer.asByteData(); } - _inputBuffer.add(new Uint8List.view( - bytes.buffer, bytes.offsetInBytes + offsetIntoIncomingBytes)); - return bytes.length; + return null; } - _ServerMessage get message { - var msgMaker = _messageTypeMap[type]; - if (msgMaker == null) { - msgMaker = () { - var msg = new _UnknownMessage()..code = type; - return msg; - }; + int addBytes(Uint8List packet) { + packets.add(packet); + + if (!hasReadHeader) { + ByteData headerBuffer = consumeNextBytes(HeaderByteSize); + if (headerBuffer == null) { + return packet.lengthInBytes; + } + + type = headerBuffer.getUint8(0); + expectedLength = headerBuffer.getUint32(1) - 4; } - _ServerMessage msg = msgMaker(); + if (expectedLength == 0) { + return packet.lengthInBytes - bytesAvailable; + } - msg.readBytes(data); + var body = consumeNextBytes(expectedLength); + if (body == null) { + return packet.lengthInBytes; + } + data = body.buffer.asUint8List(body.offsetInBytes, body.lengthInBytes); + + return packet.lengthInBytes - bytesAvailable; + } + + ServerMessage get message { + var msgMaker = messageTypeMap[type] ?? () => new UnknownMessage()..code = type; + + ServerMessage msg = msgMaker(); + msg.readBytes(data); return msg; } } -class _MessageFramer { - _MessageFrame messageInProgress = new _MessageFrame(); - List<_MessageFrame> messageQueue = []; +class MessageFramer { + MessageFrame messageInProgress = new MessageFrame(); + List messageQueue = []; void addBytes(Uint8List bytes) { var offsetIntoBytesRead = 0; do { - offsetIntoBytesRead += messageInProgress - .addBytes(new Uint8List.view(bytes.buffer, offsetIntoBytesRead)); + var byteList = new Uint8List.view(bytes.buffer, offsetIntoBytesRead); + offsetIntoBytesRead += messageInProgress.addBytes(byteList); if (messageInProgress.isComplete) { messageQueue.add(messageInProgress); - messageInProgress = new _MessageFrame(); + messageInProgress = new MessageFrame(); } } while (offsetIntoBytesRead != bytes.length); } bool get hasMessage => messageQueue.isNotEmpty; - _MessageFrame popMessage() { + MessageFrame popMessage() { return messageQueue.removeAt(0); } } diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart index db0a2a9..dfef9b2 100644 --- a/lib/src/postgresql_codec.dart +++ b/lib/src/postgresql_codec.dart @@ -1,4 +1,6 @@ -part of postgres; +import 'dart:typed_data'; +import 'dart:convert'; +import 'connection.dart'; /// The set of available data types that [PostgreSQLConnection]s support. enum PostgreSQLDataType { diff --git a/lib/src/query.dart b/lib/src/query.dart index 500ca3c..c036a84 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -1,7 +1,14 @@ -part of postgres; - -class _Query { - _Query(this.statement, this.substitutionValues, this.connection, +import 'dart:async'; +import 'postgresql_codec.dart'; +import 'connection.dart'; +import 'dart:io'; +import 'substituter.dart'; +import 'client_messages.dart'; +import 'dart:typed_data'; +import 'dart:convert'; + +class Query { + Query(this.statement, this.substitutionValues, this.connection, this.transaction); bool onlyReturnAffectedRowCount = false; @@ -12,32 +19,31 @@ class _Query { final String statement; final Map substitutionValues; - final _TransactionProxy transaction; + final PostgreSQLExecutionContext transaction; final PostgreSQLConnection connection; List specifiedParameterTypeCodes; - List<_FieldDescription> _fieldDescriptions; - - List<_FieldDescription> get fieldDescriptions => _fieldDescriptions; + List _fieldDescriptions; + List get fieldDescriptions => _fieldDescriptions; - void set fieldDescriptions(List<_FieldDescription> fds) { + void set fieldDescriptions(List fds) { _fieldDescriptions = fds; cache?.fieldDescriptions = fds; } List> rows = []; - _QueryCache cache; + QueryCache cache; void sendSimple(Socket socket) { var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues); - var queryMessage = new _QueryMessage(sqlString); + var queryMessage = new QueryMessage(sqlString); socket.add(queryMessage.asBytes()); } - void sendExtended(Socket socket, {_QueryCache cacheQuery: null}) { + void sendExtended(Socket socket, {QueryCache cacheQuery: null}) { if (cacheQuery != null) { fieldDescriptions = cacheQuery.fieldDescriptions; sendCachedQuery(socket, cacheQuery, substitutionValues); @@ -46,9 +52,9 @@ class _Query { } String statementName = (statementIdentifier ?? ""); - var formatIdentifiers = <_PostgreSQLFormatIdentifier>[]; + var formatIdentifiers = []; var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues, - replace: (_PostgreSQLFormatIdentifier identifier, int index) { + replace: (PostgreSQLFormatIdentifier identifier, int index) { formatIdentifiers.add(identifier); return "\$$index"; @@ -62,43 +68,43 @@ class _Query { .toList(); var messages = [ - new _ParseMessage(sqlString, statementName: statementName), - new _DescribeMessage(statementName: statementName), - new _BindMessage(parameterList, statementName: statementName), - new _ExecuteMessage(), - new _SyncMessage() + new ParseMessage(sqlString, statementName: statementName), + new DescribeMessage(statementName: statementName), + new BindMessage(parameterList, statementName: statementName), + new ExecuteMessage(), + new SyncMessage() ]; if (statementIdentifier != null) { - cache = new _QueryCache(statementIdentifier, formatIdentifiers); + cache = new QueryCache(statementIdentifier, formatIdentifiers); } - socket.add(_ClientMessage.aggregateBytes(messages)); + socket.add(ClientMessage.aggregateBytes(messages)); } - void sendCachedQuery(Socket socket, _QueryCache cacheQuery, + void sendCachedQuery(Socket socket, QueryCache cacheQuery, Map substitutionValues) { var statementName = cacheQuery.preparedStatementName; var parameterList = cacheQuery.orderedParameters .map((identifier) => encodeParameter(identifier, substitutionValues)) .toList(); - var bytes = _ClientMessage.aggregateBytes([ - new _BindMessage(parameterList, statementName: statementName), - new _ExecuteMessage(), - new _SyncMessage() + var bytes = ClientMessage.aggregateBytes([ + new BindMessage(parameterList, statementName: statementName), + new ExecuteMessage(), + new SyncMessage() ]); socket.add(bytes); } - _ParameterValue encodeParameter(_PostgreSQLFormatIdentifier identifier, + ParameterValue encodeParameter(PostgreSQLFormatIdentifier identifier, Map substitutionValues) { if (identifier.typeCode != null) { - return new _ParameterValue.binary( + return new ParameterValue.binary( substitutionValues[identifier.name], identifier.typeCode); } else { - return new _ParameterValue.text(substitutionValues[identifier.name]); + return new ParameterValue.text(substitutionValues[identifier.name]); } } @@ -150,12 +156,12 @@ class _Query { String toString() => statement; } -class _QueryCache { - _QueryCache(this.preparedStatementName, this.orderedParameters); +class QueryCache { + QueryCache(this.preparedStatementName, this.orderedParameters); String preparedStatementName; - List<_PostgreSQLFormatIdentifier> orderedParameters; - List<_FieldDescription> fieldDescriptions; + List orderedParameters; + List fieldDescriptions; bool get isValid { return preparedStatementName != null && @@ -164,8 +170,8 @@ class _QueryCache { } } -class _ParameterValue { - _ParameterValue.binary(dynamic value, this.postgresType) { +class ParameterValue { + ParameterValue.binary(dynamic value, this.postgresType) { isBinary = true; bytes = PostgreSQLCodec .encodeBinary(value, this.postgresType) @@ -174,7 +180,7 @@ class _ParameterValue { length = bytes?.length ?? 0; } - _ParameterValue.text(dynamic value) { + ParameterValue.text(dynamic value) { isBinary = false; if (value != null) { bytes = UTF8.encode(PostgreSQLCodec.encode(value, escapeStrings: false)); @@ -188,7 +194,7 @@ class _ParameterValue { int length; } -class _FieldDescription { +class FieldDescription { String fieldName; int tableID; int columnID; @@ -231,3 +237,57 @@ class _FieldDescription { return "$fieldName $tableID $columnID $typeID $dataTypeSize $typeModifier $formatCode"; } } + +typedef String SQLReplaceIdentifierFunction( + PostgreSQLFormatIdentifier identifier, int index); + +enum PostgreSQLFormatTokenType { text, marker } + +class PostgreSQLFormatToken { + PostgreSQLFormatToken(this.type); + + PostgreSQLFormatTokenType type; + StringBuffer buffer = new StringBuffer(); +} + +class PostgreSQLFormatIdentifier { + static Map typeStringToCodeMap = { + "text": PostgreSQLCodec.TypeText, + "int2": PostgreSQLCodec.TypeInt2, + "int4": PostgreSQLCodec.TypeInt4, + "int8": PostgreSQLCodec.TypeInt8, + "float4": PostgreSQLCodec.TypeFloat4, + "float8": PostgreSQLCodec.TypeFloat8, + "boolean": PostgreSQLCodec.TypeBool, + "date": PostgreSQLCodec.TypeDate, + "timestamp": PostgreSQLCodec.TypeTimestamp, + "timestamptz": PostgreSQLCodec.TypeTimestampTZ + }; + + static int postgresCodeForDataTypeString(String dt) { + return typeStringToCodeMap[dt]; + } + + PostgreSQLFormatIdentifier(String t) { + var components = t.split(":"); + if (components.length == 1) { + name = components.first; + } else if (components.length == 2) { + name = components.first; + + var dataTypeString = components.last; + if (dataTypeString != null) { + typeCode = postgresCodeForDataTypeString(dataTypeString); + } + } else { + throw new FormatException( + "Invalid format string identifier, must contain identifier name and optionally one data type in format '@identifier:dataType' (offending identifier: ${t})"); + } + + // Strip @ + name = name.substring(1, name.length); + } + + String name; + int typeCode; +} diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index c0ae051..f22c746 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -1,12 +1,14 @@ -part of postgres; +import 'dart:typed_data'; +import 'dart:convert'; +import 'connection.dart'; +import 'query.dart'; -abstract class _ServerMessage { +abstract class ServerMessage { void readBytes(Uint8List bytes); } -class _ErrorResponseMessage implements _ServerMessage { - PostgreSQLException generatedException; - List<_ErrorField> fields = [new _ErrorField()]; +class ErrorResponseMessage implements ServerMessage { + List fields = [new ErrorField()]; void readBytes(Uint8List bytes) { var lastByteRemovedList = @@ -18,16 +20,12 @@ class _ErrorResponseMessage implements _ServerMessage { return; } - fields.add(new _ErrorField()); + fields.add(new ErrorField()); }); - - generatedException = new PostgreSQLException._(fields); } - - String toString() => generatedException.toString(); } -class _AuthenticationMessage implements _ServerMessage { +class AuthenticationMessage implements ServerMessage { static const int KindOK = 0; static const int KindKerberosV5 = 2; static const int KindClearTextPassword = 3; @@ -52,11 +50,9 @@ class _AuthenticationMessage implements _ServerMessage { } } } - - String toString() => "Authentication: $type"; } -class _ParameterStatusMessage extends _ServerMessage { +class ParameterStatusMessage extends ServerMessage { String name; String value; @@ -65,11 +61,9 @@ class _ParameterStatusMessage extends _ServerMessage { value = UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } - - String toString() => "Parameter Message: $name $value"; } -class _ReadyForQueryMessage extends _ServerMessage { +class ReadyForQueryMessage extends ServerMessage { static const String StateIdle = "I"; static const String StateTransaction = "T"; static const String StateTransactionError = "E"; @@ -79,11 +73,9 @@ class _ReadyForQueryMessage extends _ServerMessage { void readBytes(Uint8List bytes) { state = UTF8.decode(bytes); } - - String toString() => "Ready Message: $state"; } -class _BackendKeyMessage extends _ServerMessage { +class BackendKeyMessage extends ServerMessage { int processID; int secretKey; @@ -92,12 +84,10 @@ class _BackendKeyMessage extends _ServerMessage { processID = view.getUint32(0); secretKey = view.getUint32(4); } - - String toString() => "Backend Key Message: $processID $secretKey"; } -class _RowDescriptionMessage extends _ServerMessage { - List<_FieldDescription> fieldDescriptions; +class RowDescriptionMessage extends ServerMessage { + List fieldDescriptions; void readBytes(Uint8List bytes) { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); @@ -105,18 +95,16 @@ class _RowDescriptionMessage extends _ServerMessage { var fieldCount = view.getInt16(offset); offset += 2; - fieldDescriptions = <_FieldDescription>[]; + fieldDescriptions = []; for (var i = 0; i < fieldCount; i++) { - var rowDesc = new _FieldDescription(); + var rowDesc = new FieldDescription(); offset = rowDesc.parse(view, offset); fieldDescriptions.add(rowDesc); } } - - String toString() => "RowDescription Message: $fieldDescriptions"; } -class _DataRowMessage extends _ServerMessage { +class DataRowMessage extends ServerMessage { List values = []; void readBytes(Uint8List bytes) { @@ -145,7 +133,7 @@ class _DataRowMessage extends _ServerMessage { String toString() => "Data Row Message: ${values}"; } -class _CommandCompleteMessage extends _ServerMessage { +class CommandCompleteMessage extends ServerMessage { int rowsAffected; static RegExp identifierExpression = new RegExp(r"[A-Z ]*"); @@ -160,23 +148,21 @@ class _CommandCompleteMessage extends _ServerMessage { rowsAffected = 0; } } - - String toString() => "Command Complete Message: $rowsAffected"; } -class _ParseCompleteMessage extends _ServerMessage { +class ParseCompleteMessage extends ServerMessage { void readBytes(Uint8List bytes) {} String toString() => "Parse Complete Message"; } -class _BindCompleteMessage extends _ServerMessage { +class BindCompleteMessage extends ServerMessage { void readBytes(Uint8List bytes) {} String toString() => "Bind Complete Message"; } -class _ParameterDescriptionMessage extends _ServerMessage { +class ParameterDescriptionMessage extends ServerMessage { List parameterTypeIDs; void readBytes(Uint8List bytes) { @@ -193,17 +179,15 @@ class _ParameterDescriptionMessage extends _ServerMessage { parameterTypeIDs.add(v); } } - - String toString() => "Parameter Description Message: $parameterTypeIDs"; } -class _NoDataMessage extends _ServerMessage { +class NoDataMessage extends ServerMessage { void readBytes(Uint8List bytes) {} String toString() => "No Data Message"; } -class _UnknownMessage extends _ServerMessage { +class UnknownMessage extends ServerMessage { Uint8List bytes; int code; @@ -211,5 +195,78 @@ class _UnknownMessage extends _ServerMessage { this.bytes = bytes; } - String toString() => "Unknown message: $code $bytes"; + @override + operator ==(dynamic other) { + if (bytes != null) { + if (bytes.length != other.bytes.length) { + return false; + } + for (var i = 0; i < bytes.length; i++) { + if (bytes[i] != other.bytes[i]) { + return false; + } + } + } else { + if (other.bytes != null) { + return false; + } + } + return code == other.code; + } +} + +class ErrorField { + static const int SeverityIdentifier = 83; + static const int CodeIdentifier = 67; + static const int MessageIdentifier = 77; + static const int DetailIdentifier = 68; + static const int HintIdentifier = 72; + static const int PositionIdentifier = 80; + static const int InternalPositionIdentifier = 112; + static const int InternalQueryIdentifier = 113; + static const int WhereIdentifier = 87; + static const int SchemaIdentifier = 115; + static const int TableIdentifier = 116; + static const int ColumnIdentifier = 99; + static const int DataTypeIdentifier = 100; + static const int ConstraintIdentifier = 110; + static const int FileIdentifier = 70; + static const int LineIdentifier = 76; + static const int RoutineIdentifier = 82; + + static PostgreSQLSeverity severityFromString(String str) { + switch (str) { + case "ERROR": + return PostgreSQLSeverity.error; + case "FATAL": + return PostgreSQLSeverity.fatal; + case "PANIC": + return PostgreSQLSeverity.panic; + case "WARNING": + return PostgreSQLSeverity.warning; + case "NOTICE": + return PostgreSQLSeverity.notice; + case "DEBUG": + return PostgreSQLSeverity.debug; + case "INFO": + return PostgreSQLSeverity.info; + case "LOG": + return PostgreSQLSeverity.log; + } + + return PostgreSQLSeverity.unknown; + } + + int identificationToken; + + String get text => _buffer.toString(); + StringBuffer _buffer = new StringBuffer(); + + void add(int byte) { + if (identificationToken == null) { + identificationToken = byte; + } else { + _buffer.writeCharCode(byte); + } + } } diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 5685b74..2c55fa9 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -1,22 +1,8 @@ -part of postgres; - -typedef String SQLReplaceIdentifierFunction( - _PostgreSQLFormatIdentifier identifier, int index); +import 'postgresql_codec.dart'; +import 'query.dart'; class PostgreSQLFormat { static int _AtSignCodeUnit = "@".codeUnitAt(0); - static Map _typeStringToCodeMap = { - "text": PostgreSQLCodec.TypeText, - "int2": PostgreSQLCodec.TypeInt2, - "int4": PostgreSQLCodec.TypeInt4, - "int8": PostgreSQLCodec.TypeInt8, - "float4": PostgreSQLCodec.TypeFloat4, - "float8": PostgreSQLCodec.TypeFloat8, - "boolean": PostgreSQLCodec.TypeBool, - "date": PostgreSQLCodec.TypeDate, - "timestamp": PostgreSQLCodec.TypeTimestamp, - "timestamptz": PostgreSQLCodec.TypeTimestampTZ - }; static String id(String name, {PostgreSQLDataType type: null}) { if (type != null) { @@ -57,50 +43,44 @@ class PostgreSQLFormat { return null; } - static int _postgresCodeForDataTypeString(String dt) { - return _typeStringToCodeMap[dt]; - } - static String substitute(String fmtString, Map values, {SQLReplaceIdentifierFunction replace: null}) { values ??= {}; replace ??= (spec, index) => PostgreSQLCodec.encode(values[spec.name]); - var items = <_PostgreSQLFormatToken>[]; - _PostgreSQLFormatToken lastPtr = null; + var items = []; + PostgreSQLFormatToken lastPtr = null; var iterator = new RuneIterator(fmtString); iterator.moveNext(); while (iterator.current != null) { if (lastPtr == null) { if (iterator.current == _AtSignCodeUnit) { - lastPtr = - new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); + lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.marker); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } else { - lastPtr = new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.text); + lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } - } else if (lastPtr.type == _PostgreSQLFormatTokenType.text) { + } else if (lastPtr.type == PostgreSQLFormatTokenType.text) { if (iterator.current == _AtSignCodeUnit) { - lastPtr = - new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); + lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.marker); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } else { lastPtr.buffer.writeCharCode(iterator.current); } - } else if (lastPtr.type == _PostgreSQLFormatTokenType.marker) { + } else if (lastPtr.type == PostgreSQLFormatTokenType.marker) { if (iterator.current == _AtSignCodeUnit) { iterator.movePrevious(); if (iterator.current == _AtSignCodeUnit) { lastPtr.buffer.writeCharCode(iterator.current); - lastPtr.type = _PostgreSQLFormatTokenType.text; + lastPtr.type = PostgreSQLFormatTokenType.text; } else { lastPtr = - new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.marker); + new PostgreSQLFormatToken(PostgreSQLFormatTokenType.marker); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } @@ -108,7 +88,7 @@ class PostgreSQLFormat { } else if (_isIdentifier(iterator.current)) { lastPtr.buffer.writeCharCode(iterator.current); } else { - lastPtr = new _PostgreSQLFormatToken(_PostgreSQLFormatTokenType.text); + lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); lastPtr.buffer.writeCharCode(iterator.current); items.add(lastPtr); } @@ -119,10 +99,10 @@ class PostgreSQLFormat { var idx = 1; return items.map((t) { - if (t.type == _PostgreSQLFormatTokenType.text) { + if (t.type == PostgreSQLFormatTokenType.text) { return t.buffer; } else { - var identifier = new _PostgreSQLFormatIdentifier(t.buffer.toString()); + var identifier = new PostgreSQLFormatIdentifier(t.buffer.toString()); if (!values.containsKey(identifier.name)) { throw new FormatException( @@ -155,38 +135,3 @@ class PostgreSQLFormat { (charCode == _ColonCodeUnit); } } - -enum _PostgreSQLFormatTokenType { text, marker } - -class _PostgreSQLFormatToken { - _PostgreSQLFormatToken(this.type); - - _PostgreSQLFormatTokenType type; - StringBuffer buffer = new StringBuffer(); -} - -class _PostgreSQLFormatIdentifier { - _PostgreSQLFormatIdentifier(String t) { - var components = t.split(":"); - if (components.length == 1) { - name = components.first; - } else if (components.length == 2) { - name = components.first; - - var dataTypeString = components.last; - if (dataTypeString != null) { - typeCode = - PostgreSQLFormat._postgresCodeForDataTypeString(dataTypeString); - } - } else { - throw new FormatException( - "Invalid format string identifier, must contain identifier name and optionally one data type in format '@identifier:dataType' (offending identifier: ${t})"); - } - - // Strip @ - name = name.substring(1, name.length); - } - - String name; - int typeCode; -} diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index eed2aa8..7aa408c 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -1,11 +1,11 @@ -part of postgres; +part of postgres.connection; typedef Future _TransactionQuerySignature( PostgreSQLExecutionContext connection); class _TransactionProxy implements PostgreSQLExecutionContext { _TransactionProxy(this.connection, this.executionBlock) { - beginQuery = new _Query("BEGIN", {}, connection, this) + beginQuery = new Query("BEGIN", {}, connection, this) ..onlyReturnAffectedRowCount = true; beginQuery.onComplete.future @@ -13,12 +13,12 @@ class _TransactionProxy implements PostgreSQLExecutionContext { .catchError(handleTransactionQueryError); } - _Query beginQuery; + Query beginQuery; Completer completer = new Completer(); Future get future => completer.future; - _Query get pendingQuery { + Query get pendingQuery { if (queryQueue.length > 0) { return queryQueue.first; } @@ -26,7 +26,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { return null; } - List<_Query> queryQueue = []; + List queryQueue = []; PostgreSQLConnection connection; _TransactionQuerySignature executionBlock; @@ -42,7 +42,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { "Attempting to execute query, but connection is not open."); } - var query = new _Query>>( + var query = new Query>>( fmtString, substitutionValues, connection, this); if (allowReuse) { @@ -59,7 +59,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { "Attempting to execute query, but connection is not open."); } - var query = new _Query(fmtString, substitutionValues, connection, this) + var query = new Query(fmtString, substitutionValues, connection, this) ..onlyReturnAffectedRowCount = true; return enqueue(query); @@ -93,7 +93,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { Future handleTransactionQueryError(dynamic err) async {} - Future enqueue(_Query query) async { + Future enqueue(Query query) async { queryQueue.add(query); connection._transitionToState(connection._connectionState.awake()); diff --git a/lib/src/utf8_backed_string.dart b/lib/src/utf8_backed_string.dart index b3ea969..e1b96dc 100644 --- a/lib/src/utf8_backed_string.dart +++ b/lib/src/utf8_backed_string.dart @@ -1,10 +1,12 @@ -part of postgres; +import 'dart:convert'; class UTF8BackedString { UTF8BackedString(this.string); List _cachedUTF8Bytes; + bool get hasCachedBytes => _cachedUTF8Bytes != null; + final String string; int get utf8Length { diff --git a/pubspec.yaml b/pubspec.yaml index b9fa1d8..5ffd4ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: A library to connect and query PostgreSQL databases. -version: 0.9.4 +version: 0.9.4+1 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: @@ -13,3 +13,4 @@ dependencies: dev_dependencies: test: '>=0.12.0 <0.13.0' + coverage: any diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 4c195ed..8a79dba 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -1,7 +1,8 @@ import 'dart:convert'; -import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; import 'dart:typed_data'; +import 'package:postgres/src/postgresql_codec.dart'; +import 'package:postgres/src/utf8_backed_string.dart'; void main() { test("Binary encode/decode inverse", () { @@ -171,6 +172,17 @@ void main() { expect(PostgreSQLCodec.encode(false, dataType: PostgreSQLDataType.boolean), "FALSE"); }); + + test("UTF8String caches string regardless of which method is called first", () { + var u = new UTF8BackedString("abcd"); + var v = new UTF8BackedString("abcd"); + + u.utf8Length; + v.utf8Bytes; + + expect(u.hasCachedBytes, true); + expect(v.hasCachedBytes, true); + }); } expectInverse(dynamic value, int dataType) { diff --git a/test/framer_test.dart b/test/framer_test.dart new file mode 100644 index 0000000..b15357c --- /dev/null +++ b/test/framer_test.dart @@ -0,0 +1,230 @@ +import 'package:postgres/src/message_window.dart'; +import 'package:postgres/src/server_messages.dart'; +import 'package:test/test.dart'; +import 'dart:typed_data'; +import 'dart:io'; + +void main() { + MessageFramer framer; + setUp(() { + framer = new MessageFramer(); + }); + + tearDown(() { + flush(framer); + }); + + test("Perfectly sized message in one buffer", () { + framer.addBytes(bufferWithMessages([ + messageWithBytes([1, 2, 3], 1) + ])); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]) + ]); + }); + + test("Two perfectly sized messages in one buffer", () { + framer.addBytes(bufferWithMessages([ + messageWithBytes([1, 2, 3], 1), + messageWithBytes([1, 2, 3, 4], 2) + ])); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]), + new UnknownMessage() + ..code = 2 + ..bytes = new Uint8List.fromList([1, 2, 3, 4]) + ]); + }); + + test("Header fragment", () { + var message = messageWithBytes([1, 2, 3], 1); + var fragments = fragmentedMessageBuffer(message, 2); + framer.addBytes(fragments.first); + expect(framer.messageQueue, isEmpty); + + framer.addBytes(fragments.last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]) + ]); + }); + + test("Two header fragments", () { + var message = messageWithBytes([1, 2, 3], 1); + var fragments = fragmentedMessageBuffer(message, 2); + var moreFragments = fragmentedMessageBuffer(fragments.first, 1); + + framer.addBytes(moreFragments.first); + expect(framer.messageQueue, isEmpty); + + framer.addBytes(moreFragments.last); + expect(framer.messageQueue, isEmpty); + + framer.addBytes(fragments.last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]) + ]); + }); + + test("One message + header fragment", () { + var message1 = messageWithBytes([1, 2, 3], 1); + var message2 = messageWithBytes([2, 2, 3], 2); + var message2Fragments = fragmentedMessageBuffer(message2, 3); + + framer.addBytes(bufferWithMessages([message1, message2Fragments.first])); + + expect(framer.messageQueue.length, 1); + + framer.addBytes(message2Fragments.last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]), + new UnknownMessage() + ..code = 2 + ..bytes = new Uint8List.fromList([2, 2, 3]), + ]); + }); + + test("Message + header, missing rest of buffer", () { + var message1 = messageWithBytes([1, 2, 3], 1); + var message2 = messageWithBytes([2, 2, 3], 2); + var message2Fragments = fragmentedMessageBuffer(message2, 5); + + framer.addBytes(bufferWithMessages([message1, message2Fragments.first])); + + expect(framer.messageQueue.length, 1); + + framer.addBytes(message2Fragments.last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]), + new UnknownMessage() + ..code = 2 + ..bytes = new Uint8List.fromList([2, 2, 3]), + ]); + }); + + test("Message body spans two packets", () { + var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); + var fragments = fragmentedMessageBuffer(message, 8); + framer.addBytes(fragments.first); + expect(framer.messageQueue, isEmpty); + + framer.addBytes(fragments.last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) + ]); + }); + + test("Message spans two packets, started in a packet that contained another message", () { + var earlierMessage = messageWithBytes([1, 2], 0); + var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); + + framer.addBytes(bufferWithMessages([earlierMessage, fragmentedMessageBuffer(message, 8).first])); + expect(framer.messageQueue, hasLength(1)); + + framer.addBytes(fragmentedMessageBuffer(message, 8).last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 0 + ..bytes = new Uint8List.fromList([1, 2]), + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) + ]); + }); + + test("Message spans three packets, only part of header in the first", () { + var earlierMessage = messageWithBytes([1, 2], 0); + var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 1); + + framer.addBytes(bufferWithMessages([earlierMessage, fragmentedMessageBuffer(message, 3).first])); + expect(framer.messageQueue, hasLength(1)); + + framer.addBytes(fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6).first); + expect(framer.messageQueue, hasLength(1)); + + framer.addBytes(fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6).last); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 0 + ..bytes = new Uint8List.fromList([1, 2]), + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + ]); + }); + + test("Frame with no data", () { + framer.addBytes(bufferWithMessages([messageWithBytes([], 10)])); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 10 + ]); + }); +} + +List messageWithBytes(List bytes, int messageID) { + var buffer = new BytesBuilder(); + buffer.addByte(messageID); + var lengthBuffer = new ByteData(4); + lengthBuffer.setUint32(0, bytes.length + 4); + buffer.add(lengthBuffer.buffer.asUint8List()); + buffer.add(bytes); + return buffer.toBytes(); +} + +List> fragmentedMessageBuffer(List message, int pivotPoint) { + var l1 = message.sublist(0, pivotPoint); + var l2 = message.sublist(pivotPoint, message.length); + return [l1, l2]; +} + +List bufferWithMessages(List> messages) { + return new Uint8List.fromList(messages.expand((l) => l).toList()); +} + +flush(MessageFramer framer) { + framer.messageQueue = []; + framer.addBytes(bufferWithMessages([ + messageWithBytes([1, 2, 3], 1) + ])); + + var messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [ + new UnknownMessage() + ..code = 1 + ..bytes = new Uint8List.fromList([1, 2, 3]) + ]); +} \ No newline at end of file diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 8f74e15..1e1e500 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:postgres/postgres.dart'; +import 'package:postgres/src/substituter.dart'; import 'package:test/test.dart'; void main() { diff --git a/test/query_test.dart b/test/query_test.dart index 76ef30b..38a7d81 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -1,5 +1,6 @@ import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; +import 'package:postgres/src/postgresql_codec.dart'; void main() { group("Successful queries", () { @@ -49,6 +50,18 @@ void main() { expect(result, [expectedRow]); }); + test("Really long raw substitution value", () async { + var result = await connection.query("INSERT INTO t (t) VALUES (${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)}) returning t;", substitutionValues: { + "t" : lorumIpsum + }); + expect(result, [[lorumIpsum]]); + }); + + test("Really long SQL string in execute", () async { + var result = await connection.execute("INSERT INTO t (t) VALUES ('$lorumIpsum') returning t;"); + expect(result, 1); + }); + test("Query without specifying types", () async { var result = await connection.query( "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " @@ -294,3 +307,118 @@ void main() { }); }); } + +const String lorumIpsum = """Lorem + ipsum dolor sit amet, consectetur adipiscing elit. Quisque in accumsan + felis. Nunc semper velit purus, a pellentesque mauris aliquam ut. Sed + laoreet iaculis nunc sit amet dignissim. Aenean venenatis sollicitudin + justo, quis imperdiet diam fringilla quis. Fusce nec mauris imperdiet + dui iaculis consequat. Integer convallis justo a neque finibus imperdiet + et nec sem. In laoreet quis ante eget pellentesque. Nunc posuere faucibus + nibh eu aliquet. Aliquam rutrum posuere nisi, ut maximus mauris tincidunt + at. Integer fermentum venenatis viverra. Vivamus non magna malesuada, + ullamcorper neque ut, auctor justo. Donec ut mattis elit, eget varius urna. + Vestibulum consectetur aliquet semper. Nullam pellentesque nunc quis risus + rutrum viverra. Fusce porta tortor in neque maximus efficitur. Aenean + euismod sollicitudin neque a tristique. Donec consequat egestas vulputate. + Pellentesque ultricies pellentesque ex pellentesque gravida. Praesent + lacinia tortor vitae dolor vehicula iaculis. In sed egestas lacus, eget + semper mauris. Sed augue augue, vehicula eu ornare quis, egestas id libero. + Sed quis enim lobortis, sollicitudin nibh eu, maximus justo. Nam mauris + tortor, suscipit dapibus sodales non, suscipit eu felis. Nam pellentesque + eleifend risus rhoncus facilisis. Vestibulum commodo fringilla enim tempus + hendrerit. Quisque a est varius, efficitur magna ac, condimentum metus. + In quam nisi, facilisis at pulvinar vitae, placerat quis est. Duis sagittis + non leo id placerat. Integer lobortis tellus rhoncus mi gravida, vel posuere + eros convallis. Suspendisse finibus elit viverra purus dictum, eget ultrices + risus hendrerit. Sed fermentum elit eu nibh pellentesque, eget suscipit + purus malesuada. Duis quis convallis quam, vel rutrum metus. Sed pulvinar + nisi non mauris laoreet, a faucibus turpis euismod. Cras et arcu hendrerit, + commodo elit eget, gravida lectus. Nulla euismod erat id venenatis sodales. + Duis non dolor facilisis, egestas felis pellentesque, porttitor augue. + Vestibulum eu tincidunt sapien, volutpat lobortis mi. Cum sociis natoque + penatibus et magnis dis parturient montes, nascetur ridiculus mus. + Praesent nec rhoncus erat, molestie imperdiet magna. Quisque vel eleifend + lectus. Cras ut orci et sem pellentesque pharetra. Donec ac urna sit amet + est viverra placerat. Duis sit amet ipsum venenatis, aliquam mauris quis, + fringilla leo. Suspendisse potenti. Cum sociis natoque penatibus et magnis + dis parturient montes, nascetur ridiculus mus. Sed eu condimentum nisi, + lobortis mollis est. Nam auctor auctor enim sit amet tincidunt. Proin + hendrerit volutpat vestibulum. Fusce facilisis rutrum pretium. Proin eget + imperdiet elit. Phasellus vulputate ex malesuada porttitor lobortis. + Curabitur vitae orci et lacus condimentum varius fringilla blandit metus. + Class aptent taciti sociosqu ad litora torquent per conubia nostra, per + inceptos himenaeos. Suspendisse vehicula mauris in libero finibus bibendum. + Phasellus ligula odio, pharetra vel metus maximus, efficitur pretium erat. + Morbi mi purus, sagittis quis congue et, pharetra id mauris. Cras eget neque + id erat cursus pellentesque et sed ipsum. In vel nibh at nulla pellentesque + elementum. Cras ultricies molestie massa, nec consequat urna scelerisque eu. + Etiam varius fermentum mi non tincidunt. Pellentesque vel elit id turpis + lobortis ullamcorper et a lorem. Nunc purus nulla, feugiat vitae congue + imperdiet, auctor sit amet ante. Nulla facilisi. Donec luctus sem vel diam + fringilla, vel fermentum augue placerat. Suspendisse et eros dignissim ipsum + vestibulum elementum. Curabitur scelerisque tortor sit amet libero pharetra + condimentum. Maecenas molestie non erat sed blandit. Ut lectus est, consequat + a auctor in, vulputate ac mi. Sed sem tortor, consectetur eget tincidunt et, + iaculis non diam. Praesent quis ipsum sem. Nulla lobortis nec ex non facilisis. + Aliquam porttitor metus eu velit convallis volutpat. Duis nec euismod urna. + Nullam molestie ligula urna, non laoreet mi facilisis quis. Donec aliquam + eget diam sit amet facilisis. Sed suscipit, justo non congue fringilla, + augue tellus volutpat velit, a dignissim felis quam sit amet metus. + Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis + malesuada cursus dolor, eget aliquam leo ultricies at. Fusce fringilla + sed quam id finibus. Suspendisse ullamcorper, urna non feugiat elementum, + neque tortor suscipit elit, id condimentum lacus augue ut massa. Lorem + ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit + amet, consectetur adipiscing elit. Mauris tempor faucibus ipsum, vitae + blandit libero sollicitudin nec. Cras elementum mauris id ipsum tempus + ullamcorper. Class aptent taciti sociosqu ad litora torquent per conubia + nostra, per inceptos himenaeos. Donec vehicula, sapien sit amet pulvinar + pretium, elit mauris finibus nunc, ac pellentesque justo dolor eu dui. + Nulla tincidunt porttitor semper. Maecenas nunc enim, feugiat vel ex a, + pulvinar lacinia dolor. Donec in tortor ac justo porta malesuada et nec + ante. Maecenas vel bibendum nunc. Ut sollicitudin elementum orci ac auctor. + Duis blandit quam quis dapibus rhoncus. Proin sagittis feugiat mi ac + consequat. Sed maximus sodales diam id luctus. In cursus dictum rutrum. + Vestibulum vitae enim odio. Morbi non pharetra sem, at molestie lorem. + Nam libero est, imperdiet at aliquam vitae, mollis eget erat. Vivamus + eu nisi auctor, pharetra ligula nec, rhoncus augue. Quisque viverra + mollis velit, nec euismod lectus sagittis eget. Curabitur sed augue + vestibulum, luctus dolor nec, ornare ligula. Fusce lectus nunc, + tincidunt ut felis sed, placerat molestie risus. Etiam vel libero tellus. + Quisque elementum turpis non tempus dignissim. Pellentesque consectetur + tellus et urna ultrices elementum. Proin feugiat mi eu cursus mattis. + Proin tincidunt tincidunt turpis, in vulputate mauris. Cras posuere + lorem in erat lobortis sollicitudin. Proin in pulvinar diam, in convallis + urna. Praesent eget quam non velit dapibus tempus. Maecenas molestie nec + magna id auctor. Integer in sem non arcu dapibus iaculis. Sed eget massa + est. Cras dictum erat vel rutrum suscipit. In vehicula lorem non tempus + dignissim. Praesent gravida condimentum sem id elementum. Duis laoreet, + diam quis imperdiet mollis, nulla erat dapibus nisl, ac varius ex quam + id purus. Donec dignissim nulla lacinia eros venenatis tempor. Proin purus + lacus, ultrices non sodales quis, commodo et metus. Duis ante massa, + faucibus nec pharetra ut, ultricies et turpis. Morbi volutpat hendrerit + lacus, ut vehicula nibh tempor eget. Cras quis iaculis nisi, sit amet + placerat orci. Nam scelerisque velit malesuada, iaculis urna et, condimentum + dui. Nulla convallis augue vitae consequat laoreet. Quisque fermentum + ullamcorper magna, ut aliquam nunc facilisis in. Praesent tempus ullamcorper + massa, et fermentum purus bibendum quis. Sed sed venenatis odio, eget + euismod nisl. Nam et imperdiet dolor. Nam convallis justo a diam ultrices + gravida quis vel sapien. Vivamus aliquet lobortis augue ut accumsan. Donec + mi dolor, bibendum in mattis nec, porta vitae tellus. Donec eu tincidunt + lectus. Fusce placerat euismod turpis, et porta ligula tincidunt non. + Cras ac vestibulum diam. Cras eu quam finibus, feugiat libero vel, ornare + purus. Duis consectetur dictum metus non cursus. Vestibulum semper id erat + eget bibendum. Etiam vitae dui quis justo pretium pellentesque. Aenean sed + tellus eu odio volutpat consectetur condimentum vel leo. Etiam vulputate + risus tellus, at viverra enim vulputate vel. Mauris eu tortor nulla. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere + cubilia Curae; Nam ac nulla in ex lobortis tincidunt at non urna. Donec + congue lectus ut mauris eleifend cursus. Interdum et malesuada fames ac ante + ipsum primis in faucibus. Mauris sit amet porta mi, non mollis dui. Nullam + cursus sapien at pretium porta. Donec ac mauris pharetra, vehicula dolor + nec, lacinia mauris. Aliquam et felis finibus, cursus neque a, viverra sem. + Pellentesque habitant morbi tristique senectus et netus et malesuada fames + ac turpis egestas. Proin malesuada orci sit amet neque dapibus bibendum. + In lobortis imperdiet condimentum. Nullam est nisi, efficitur ac consectetur + eu, efficitur a libero. In nullam."""; \ No newline at end of file diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 0f6588f..3eeb366 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -1,8 +1,6 @@ import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; -import 'dart:io'; import 'dart:async'; -import 'dart:mirrors'; void main() { group("Transaction behavior", () { @@ -293,7 +291,7 @@ void main() { await c.query("INSERT INTO t (id) VALUES (1)"); }); expect(true, false); - } on PostgreSQLException catch (e) {} + } on PostgreSQLException {} var result = await conn.transaction((ctx) async { return await ctx.query("SELECT id FROM t"); From 066b6f1a1f3b7b46cba4528f1920fd27882f15c5 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Thu, 19 Jan 2017 10:24:43 -0500 Subject: [PATCH 08/73] Fix script (#12) --- ci/script.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/script.sh b/ci/script.sh index 4510e99..6252f29 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -1,7 +1,7 @@ #!/bin/bash - +set -e pub run test -j 1 -r expanded if [[ "$TRAVIS_BRANCH" == "master" ]]; then pub global activate -sgit https://github.com/stablekernel/codecov_dart.git dart_codecov_generator --report-on=lib/ --verbose --no-html -fi \ No newline at end of file +fi From f09c4bf13a7a0f303f48eed1e0cb60892cd5eee7 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 15 Mar 2017 15:56:55 -0400 Subject: [PATCH 09/73] Adds SSL support (#14) * Updating tests * Timeout handling * timeout * Fix test, update exception string * dartfmt, changelog, pubspec * Try diff config * Update to postgresql 9.5 * Diff config * trying config * Testing * Update script for 9.5 * remove debug statements * Adding spaces to exception toString * remove comment * remove unneeded import --- .travis.yml | 4 +- CHANGELOG.md | 4 + lib/src/connection.dart | 71 ++++++++++---- lib/src/exceptions.dart | 27 +++++- lib/src/message_window.dart | 26 ++++-- pubspec.yaml | 4 +- test/connection_test.dart | 180 ++++++++++++++++++++++++++++++++++++ test/encoding_test.dart | 3 +- test/framer_test.dart | 31 ++++--- test/query_test.dart | 15 +-- 10 files changed, 314 insertions(+), 51 deletions(-) diff --git a/.travis.yml b/.travis.yml index a09aaa7..bd4388d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: dart sudo: required addons: - postgresql: "9.4" + postgresql: "9.5" services: - postgresql before_script: - - sudo cp ci/pg_hba.conf /etc/postgresql/9.4/main/pg_hba.conf + - sudo cp ci/pg_hba.conf /etc/postgresql/9.5/main/pg_hba.conf - sudo /etc/init.d/postgresql restart - psql -c 'create database dart_test;' -U postgres - psql -c 'create user dart with createdb;' -U postgres diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c630c..59d5475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.5 + +- Allow connect via SSL. + ## 0.9.4 - Fixed issue with buffer length diff --git a/lib/src/connection.dart b/lib/src/connection.dart index d6d9246..7e9f9e3 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -1,6 +1,8 @@ library postgres.connection; import 'dart:async'; +import 'dart:typed_data'; + import 'message_window.dart'; import 'query.dart'; @@ -152,34 +154,25 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { } _hasConnectedPreviously = true; + _socket = await Socket + .connect(host, port) + .timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); + _framer = new MessageFramer(); if (useSSL) { - _socket = await SecureSocket - .connect(host, port) - .timeout(new Duration(seconds: timeoutInSeconds)); - } else { - _socket = await Socket - .connect(host, port) - .timeout(new Duration(seconds: timeoutInSeconds)); + _socket = await _upgradeSocketToSSL(_socket, timeout: timeoutInSeconds); } - _framer = new MessageFramer(); + var connectionComplete = new Completer(); + _socket.listen(_readData, onError: _handleSocketError, onDone: _handleSocketClosed); - var connectionComplete = new Completer(); _transitionToState( new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); return connectionComplete.future - .timeout(new Duration(seconds: timeoutInSeconds), onTimeout: () { - _connectionState = new _PostgreSQLConnectionStateClosed(); - _socket?.destroy(); - - _cancelCurrentQueries(); - throw new PostgreSQLException( - "Timed out trying to connect to database postgres://$host:$port/$databaseName."); - }); + .timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); } /// Closes a connection. @@ -298,6 +291,15 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { //////// + void _timeout() { + _connectionState = new _PostgreSQLConnectionStateClosed(); + _socket?.destroy(); + + _cancelCurrentQueries(); + throw new PostgreSQLException( + "Timed out trying to connect to database postgres://$host:$port/$databaseName."); + } + Future _enqueue(Query query) async { _queryQueue.add(query); _transitionToState(_connectionState.awake()); @@ -381,6 +383,41 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _cancelCurrentQueries(); } + Future _upgradeSocketToSSL(Socket originalSocket, + {int timeout: 30}) async { + var sslCompleter = new Completer(); + + originalSocket.listen((data) { + if (data.length != 1) { + sslCompleter.completeError(new PostgreSQLException( + "Could not initalize SSL connection, received unknown byte stream.")); + return; + } + + sslCompleter.complete(data.first); + }, + onDone: () => sslCompleter.completeError(new PostgreSQLException( + "Could not initialize SSL connection, connection closed during handshake.")), + onError: (err) { + sslCompleter.completeError(err); + }); + + var byteBuffer = new ByteData(8); + byteBuffer.setUint32(0, 8); + byteBuffer.setUint32(4, 80877103); + originalSocket.add(byteBuffer.buffer.asUint8List()); + + var responseByte = await sslCompleter.future + .timeout(new Duration(seconds: timeout), onTimeout: _timeout); + if (responseByte == 83) { + return SecureSocket + .secure(originalSocket, onBadCertificate: (certificate) => true) + .timeout(new Duration(seconds: timeout), onTimeout: _timeout); + } + + throw new PostgreSQLException("SSL not allowed for this connection."); + } + void _cacheQuery(Query query) { if (query.cache == null) { return; diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index 09bdd60..153905f 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -111,6 +111,29 @@ class PostgreSQLException implements Exception { /// A [StackTrace] if available. StackTrace stackTrace; - String toString() => - "$severity $code: $message Detail: $detail Hint: $hint Table: $tableName Column: $columnName Constraint: $constraintName"; + String toString() { + var buff = new StringBuffer("$severity $code: $message "); + + if (detail != null) { + buff.write("Detail: $detail "); + } + + if (hint != null) { + buff.write("Hint: $hint "); + } + + if (tableName != null) { + buff.write("Table: $tableName "); + } + + if (columnName != null) { + buff.write("Column: $columnName "); + } + + if (constraintName != null) { + buff.write("Constraint $constraintName "); + } + + return buff.toString(); + } } diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index a1c3663..6d6adb5 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; import 'dart:io'; import 'server_messages.dart'; -import 'dart:math'; class MessageFrame { static const int HeaderByteSize = 5; @@ -41,15 +40,18 @@ class MessageFrame { // remove & return it. if (firstPacket.lengthInBytes == length) { packets.removeAt(0); - return firstPacket.buffer.asByteData(firstPacket.offsetInBytes, firstPacket.lengthInBytes); + return firstPacket.buffer + .asByteData(firstPacket.offsetInBytes, firstPacket.lengthInBytes); } if (firstPacket.lengthInBytes > length) { // We have to split up this packet and remove & return the first portion of it, // and replace it with the second portion of it. var remainingOffset = firstPacket.offsetInBytes + length; - var bytesNeeded = firstPacket.buffer.asByteData(firstPacket.offsetInBytes, length); - var bytesRemaining = firstPacket.buffer.asUint8List(remainingOffset, firstPacket.lengthInBytes - length); + var bytesNeeded = + firstPacket.buffer.asByteData(firstPacket.offsetInBytes, length); + var bytesRemaining = firstPacket.buffer + .asUint8List(remainingOffset, firstPacket.lengthInBytes - length); packets.removeAt(0); packets.insert(0, bytesRemaining); @@ -65,15 +67,20 @@ class MessageFrame { var builder = new BytesBuilder(copy: false); var bytesNeeded = length - builder.length; - while(bytesNeeded > 0) { + while (bytesNeeded > 0) { var packet = packets.removeAt(0); var bytesRemaining = packet.lengthInBytes; if (bytesRemaining <= bytesNeeded) { - builder.add(packet.buffer.asUint8List(packet.offsetInBytes, packet.lengthInBytes)); + builder.add(packet.buffer + .asUint8List(packet.offsetInBytes, packet.lengthInBytes)); } else { - builder.add(packet.buffer.asUint8List(packet.offsetInBytes, bytesNeeded)); - packets.insert(0, packet.buffer.asUint8List(bytesNeeded, bytesRemaining - bytesNeeded)); + builder.add( + packet.buffer.asUint8List(packet.offsetInBytes, bytesNeeded)); + packets.insert( + 0, + packet.buffer + .asUint8List(bytesNeeded, bytesRemaining - bytesNeeded)); } bytesNeeded = length - builder.length; @@ -113,7 +120,8 @@ class MessageFrame { } ServerMessage get message { - var msgMaker = messageTypeMap[type] ?? () => new UnknownMessage()..code = type; + var msgMaker = + messageTypeMap[type] ?? () => new UnknownMessage()..code = type; ServerMessage msg = msgMaker(); msg.readBytes(data); diff --git a/pubspec.yaml b/pubspec.yaml index 5ffd4ac..747557f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres -description: A library to connect and query PostgreSQL databases. -version: 0.9.4+1 +description: PostgreSQL database driver. Supports statement reuse and binary protocol. +version: 0.9.5 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: diff --git a/test/connection_test.dart b/test/connection_test.dart index fe273c1..47efabe 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -21,6 +21,21 @@ void main() { expect(await conn.execute("select 1"), equals(1)); }); + test("SSL Connect with md5 auth required", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart", useSSL: true); + + await conn.open(); + + expect(await conn.execute("select 1"), equals(1)); + var socketMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_socket")); + var underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee; + expect(underlyingSocket is SecureSocket, true); + }); + test("Connect with no auth required", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); @@ -29,6 +44,14 @@ void main() { expect(await conn.execute("select 1"), equals(1)); }); + test("SSL Connect with no auth required", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); + await conn.open(); + + expect(await conn.execute("select 1"), equals(1)); + }); + test("Closing idle connection succeeds, closes underlying socket", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", @@ -47,6 +70,24 @@ void main() { conn = null; }); + test("SSL Closing idle connection succeeds, closes underlying socket", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); + await conn.open(); + + await conn.close(); + + var socketMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_socket")); + Socket underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee; + expect(await underlyingSocket.done, isNotNull); + + conn = null; + }); + test( "Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", () async { @@ -71,6 +112,31 @@ void main() { expect(e.message, contains("Connection closed")); } }); + + test( + "SSL Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); + await conn.open(); + + var futures = [ + conn.query("select 1", allowReuse: false), + conn.query("select 2", allowReuse: false), + conn.query("select 3", allowReuse: false), + conn.query("select 4", allowReuse: false), + conn.query("select 5", allowReuse: false) + ]; + + await conn.close(); + + try { + await Future.wait(futures); + expect(true, false); + } on PostgreSQLException catch (e) { + expect(e.message, contains("Connection closed")); + } + }); }); group("Successful queries over time", () { @@ -169,6 +235,19 @@ void main() { } }); + test("SSL Sending queries to opening connection triggers error", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); + conn.open(); + + try { + await conn.execute("select 1"); + expect(true, false); + } on PostgreSQLException catch (e) { + expect(e.message, contains("connection is not open")); + } + }); + test("Starting transaction while opening connection triggers error", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", @@ -185,6 +264,22 @@ void main() { } }); + test("SSL Starting transaction while opening connection triggers error", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); + conn.open(); + + try { + await conn.transaction((ctx) async { + await ctx.execute("select 1"); + }); + expect(true, false); + } on PostgreSQLException catch (e) { + expect(e.message, contains("connection is not open")); + } + }); + test("Invalid password reports error, conn is closed, disables conn", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", @@ -200,6 +295,21 @@ void main() { await expectConnectionIsInvalid(conn); }); + test("SSL Invalid password reports error, conn is closed, disables conn", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "notdart", useSSL: true); + + try { + await conn.open(); + expect(true, false); + } on PostgreSQLException catch (e) { + expect(e.message, contains("password authentication failed")); + } + + await expectConnectionIsInvalid(conn); + }); + test("A query error maintains connectivity, allows future queries", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", @@ -346,6 +456,20 @@ void main() { await expectConnectionIsInvalid(conn); }); + test( + "SSL Socket fails to connect reports error, disables connection for future use", + () async { + var conn = new PostgreSQLConnection("localhost", 5431, "dart_test", + useSSL: true); + + try { + await conn.open(); + expect(true, false); + } on SocketException {} + + await expectConnectionIsInvalid(conn); + }); + test( "Connection that times out throws appropriate error and cannot be reused", () async { @@ -369,6 +493,29 @@ void main() { await expectConnectionIsInvalid(conn); }); + test( + "SSL Connection that times out throws appropriate error and cannot be reused", + () async { + serverSocket = + await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket.listen((s) { + socket = s; + // Don't respond on purpose + s.listen((bytes) {}); + }); + + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2, useSSL: true); + + try { + await conn.open(); + } on PostgreSQLException catch (e) { + expect(e.message, contains("Timed out trying to connect")); + } + + await expectConnectionIsInvalid(conn); + }); + test("Connection that times out triggers future for pending queries", () async { var openCompleter = new Completer(); @@ -396,6 +543,39 @@ void main() { expect(e.message, contains("closed or query cancelled")); } }); + + test("SSL Connection that times out triggers future for pending queries", + () async { + var openCompleter = new Completer(); + serverSocket = + await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket.listen((s) { + socket = s; + // Don't respond on purpose + s.listen((bytes) {}); + new Future.delayed(new Duration(milliseconds: 100), () { + openCompleter.complete(); + }); + }); + + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2, useSSL: true); + conn.open().catchError((e) {}); + + await openCompleter.future; + + try { + await conn.execute("select 1"); + expect(true, false); + } on PostgreSQLException catch (e) { + expect(e.message, contains("but connection is not open")); + } + + try { + await conn.open(); + expect(true, false); + } on PostgreSQLException {} + }); }); } diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 8a79dba..9a10464 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -173,7 +173,8 @@ void main() { "FALSE"); }); - test("UTF8String caches string regardless of which method is called first", () { + test("UTF8String caches string regardless of which method is called first", + () { var u = new UTF8BackedString("abcd"); var v = new UTF8BackedString("abcd"); diff --git a/test/framer_test.dart b/test/framer_test.dart index b15357c..28f0fb1 100644 --- a/test/framer_test.dart +++ b/test/framer_test.dart @@ -141,11 +141,14 @@ void main() { ]); }); - test("Message spans two packets, started in a packet that contained another message", () { + test( + "Message spans two packets, started in a packet that contained another message", + () { var earlierMessage = messageWithBytes([1, 2], 0); var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); - framer.addBytes(bufferWithMessages([earlierMessage, fragmentedMessageBuffer(message, 8).first])); + framer.addBytes(bufferWithMessages( + [earlierMessage, fragmentedMessageBuffer(message, 8).first])); expect(framer.messageQueue, hasLength(1)); framer.addBytes(fragmentedMessageBuffer(message, 8).last); @@ -163,15 +166,21 @@ void main() { test("Message spans three packets, only part of header in the first", () { var earlierMessage = messageWithBytes([1, 2], 0); - var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 1); + var message = + messageWithBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 1); - framer.addBytes(bufferWithMessages([earlierMessage, fragmentedMessageBuffer(message, 3).first])); + framer.addBytes(bufferWithMessages( + [earlierMessage, fragmentedMessageBuffer(message, 3).first])); expect(framer.messageQueue, hasLength(1)); - framer.addBytes(fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6).first); + framer.addBytes( + fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6) + .first); expect(framer.messageQueue, hasLength(1)); - framer.addBytes(fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6).last); + framer.addBytes( + fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6) + .last); var messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ @@ -180,7 +189,8 @@ void main() { ..bytes = new Uint8List.fromList([1, 2]), new UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + ..bytes = + new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) ]); }); @@ -188,10 +198,7 @@ void main() { framer.addBytes(bufferWithMessages([messageWithBytes([], 10)])); var messages = framer.messageQueue.map((f) => f.message).toList(); - expect(messages, [ - new UnknownMessage() - ..code = 10 - ]); + expect(messages, [new UnknownMessage()..code = 10]); }); } @@ -227,4 +234,4 @@ flush(MessageFramer framer) { ..code = 1 ..bytes = new Uint8List.fromList([1, 2, 3]) ]); -} \ No newline at end of file +} diff --git a/test/query_test.dart b/test/query_test.dart index 38a7d81..1e37985 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -51,14 +51,17 @@ void main() { }); test("Really long raw substitution value", () async { - var result = await connection.query("INSERT INTO t (t) VALUES (${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)}) returning t;", substitutionValues: { - "t" : lorumIpsum - }); - expect(result, [[lorumIpsum]]); + var result = await connection.query( + "INSERT INTO t (t) VALUES (${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)}) returning t;", + substitutionValues: {"t": lorumIpsum}); + expect(result, [ + [lorumIpsum] + ]); }); test("Really long SQL string in execute", () async { - var result = await connection.execute("INSERT INTO t (t) VALUES ('$lorumIpsum') returning t;"); + var result = await connection + .execute("INSERT INTO t (t) VALUES ('$lorumIpsum') returning t;"); expect(result, 1); }); @@ -421,4 +424,4 @@ const String lorumIpsum = """Lorem Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin malesuada orci sit amet neque dapibus bibendum. In lobortis imperdiet condimentum. Nullam est nisi, efficitur ac consectetur - eu, efficitur a libero. In nullam."""; \ No newline at end of file + eu, efficitur a libero. In nullam."""; From c8cfb31d67f51de558e2a9b59148464e9c780849 Mon Sep 17 00:00:00 2001 From: AndrewSt Date: Sun, 2 Jul 2017 17:06:35 +0300 Subject: [PATCH 10/73] Add support notification from database (#18) * fix Dart Analysis warning * add support notification from database --- lib/src/connection.dart | 13 +++- lib/src/connection_fsm.dart | 4 +- lib/src/message_window.dart | 1 + lib/src/query.dart | 2 +- lib/src/server_messages.dart | 21 ++++++ test/notification_test.dart | 124 +++++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 test/notification_test.dart diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 7e9f9e3..604ce75 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -79,6 +79,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _connectionState.connection = this; } + final StreamController _notifications = new StreamController.broadcast(); // Add flag for debugging that captures stack trace prior to execution /// Hostname of database this connection refers to. @@ -105,6 +106,12 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// The timezone of this connection for date operations that don't specify a timezone. String timeZone; + /// The processID of this backend. + int processID; + + /// The notifications from the database + Stream get notifications => _notifications.stream; + /// Whether or not this connection is open or not. /// /// This is [true] when this instance is first created and after it has been closed or encountered an unrecoverable error. @@ -124,7 +131,6 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { Map _reuseMap = {}; int _reuseCounter = 0; - int _processID; int _secretKey; List _salt; @@ -183,6 +189,8 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { await _socket?.close(); + await _notifications.close(); + _cancelCurrentQueries(); } @@ -361,6 +369,9 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { try { if (msg is ErrorResponseMessage) { _transitionToState(_connectionState.onErrorResponse(msg)); + } else if (msg is NotificationResponseMessage) { + _notifications.add( + new Notification(msg.processID, msg.channel, msg.payload)); } else { _transitionToState(_connectionState.onMessage(msg)); } diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index b37949c..9eef54c 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -115,7 +115,7 @@ class _PostgreSQLConnectionStateAuthenticating connection.settings[message.name] = message.value; } else if (message is BackendKeyMessage) { connection._secretKey = message.secretKey; - connection._processID = message.processID; + connection.processID = message.processID; } else if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateIdle) { return new _PostgreSQLConnectionStateIdle(openCompleter: completer); @@ -149,7 +149,7 @@ class _PostgreSQLConnectionStateAuthenticated connection.settings[message.name] = message.value; } else if (message is BackendKeyMessage) { connection._secretKey = message.secretKey; - connection._processID = message.processID; + connection.processID = message.processID; } else if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateIdle) { return new _PostgreSQLConnectionStateIdle(openCompleter: completer); diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 6d6adb5..4125356 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -7,6 +7,7 @@ class MessageFrame { static Map messageTypeMap = { 49: () => new ParseCompleteMessage(), 50: () => new BindCompleteMessage(), + 65: () => new NotificationResponseMessage(), 67: () => new CommandCompleteMessage(), 68: () => new DataRowMessage(), 69: () => new ErrorResponseMessage(), diff --git a/lib/src/query.dart b/lib/src/query.dart index c036a84..6bc87f4 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -13,7 +13,7 @@ class Query { bool onlyReturnAffectedRowCount = false; String statementIdentifier; - Completer onComplete = new Completer.sync(); + Completer onComplete = new Completer.sync(); Future get future => onComplete.future; diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index f22c746..d7ae66c 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -133,6 +133,19 @@ class DataRowMessage extends ServerMessage { String toString() => "Data Row Message: ${values}"; } +class NotificationResponseMessage extends ServerMessage { + int processID; + String channel; + String payload; + + void readBytes(Uint8List bytes) { + var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); + processID = view.getUint32(0); + channel = UTF8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); + payload = UTF8.decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); + } +} + class CommandCompleteMessage extends ServerMessage { int rowsAffected; @@ -270,3 +283,11 @@ class ErrorField { } } } + +class Notification { + final int processID; + final String channel; + final String payload; + + Notification(this.processID, this.channel, this.payload); +} diff --git a/test/notification_test.dart b/test/notification_test.dart new file mode 100644 index 0000000..1fd2df1 --- /dev/null +++ b/test/notification_test.dart @@ -0,0 +1,124 @@ +import 'dart:async'; + +import 'package:postgres/postgres.dart'; +import 'package:test/test.dart'; + +void main() { + group("Successful notifications", () { + var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); + + setUp(() async { + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); + await connection.open(); + }); + + tearDown(() async { + await connection.close(); + }); + + test("Notification Response", () async { + var channel = 'virtual'; + var payload = 'This is the payload'; + var futureMsg = connection.notifications.first; + await connection + .execute("LISTEN $channel;" + "NOTIFY $channel, '$payload';"); + + var msg = await futureMsg + .timeout(new Duration(milliseconds: 200)); + expect(msg.channel, channel); + expect(msg.payload, payload); + }); + + test("Notification Response empty payload", () async { + var channel = 'virtual'; + var futureMsg = connection.notifications.first; + await connection + .execute("LISTEN $channel;" + "NOTIFY $channel;"); + + var msg = await futureMsg + .timeout(new Duration(milliseconds: 200)); + expect(msg.channel, channel); + expect(msg.payload, ''); + }); + + test("Notification UNLISTEN", () async { + var channel = 'virtual'; + var payload = 'This is the payload'; + var futureMsg = connection.notifications.first; + await connection + .execute("LISTEN $channel;" + "NOTIFY $channel, '$payload';"); + + var msg = await futureMsg + .timeout(new Duration(milliseconds: 200)); + + expect(msg.channel, channel); + expect(msg.payload, payload); + + await connection + .execute("UNLISTEN $channel;"); + + futureMsg = connection.notifications.first; + + try { + await connection + .execute("NOTIFY $channel, '$payload';"); + + await futureMsg + .timeout(new Duration(milliseconds: 200)); + + fail('There should be no notification'); + } on TimeoutException catch (e) {} + }); + + test("Notification many channel", () async { + Map countResponse = new Map(); + int totalCountResponse = 0; + Completer finishExecute = new Completer(); + connection.notifications.listen((msg){ + int count = countResponse[msg.channel]; + countResponse[msg.channel] = (count ?? 0) + 1; + totalCountResponse++; + if(totalCountResponse == 20) + finishExecute.complete(); + }); + + var channel1 = 'virtual1'; + var channel2 = 'virtual2'; + + var notifier = () async { + for (int i = 0; i < 5; i++) { + await connection + .execute("NOTIFY $channel1;" + "NOTIFY $channel2;"); + } + }; + + await connection + .execute("LISTEN $channel1;"); + await notifier(); + + await connection + .execute("LISTEN $channel2;"); + await notifier(); + + await connection + .execute("UNLISTEN $channel1;"); + await notifier(); + + await connection + .execute("UNLISTEN $channel2;"); + await notifier(); + + await finishExecute.future + .timeout(new Duration(milliseconds: 200)); + + expect(countResponse[channel1], 10); + expect(countResponse[channel2], 10); + }, timeout: new Timeout(new Duration(seconds: 5))); + }); +} From 42278fe95ac8ed1d8c71300bc25166e6cf553338 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Sun, 2 Jul 2017 11:50:45 -0400 Subject: [PATCH 11/73] Better reporting for errors (#16) * collect error and stack trace * Fix type issue * pubspec/changelog --- CHANGELOG.md | 4 ++++ lib/src/connection.dart | 12 ++++++------ lib/src/query.dart | 10 +++++----- lib/src/transaction_proxy.dart | 2 +- pubspec.yaml | 4 ++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d5475..d519c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.6 + +- Adds better error reporting. + ## 0.9.5 - Allow connect via SSL. diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 604ce75..abdcf79 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -327,7 +327,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { return result; } - void _cancelCurrentQueries() { + void _cancelCurrentQueries([Object error, StackTrace stackTrace]) { var queries = _queryQueue; _queryQueue = []; @@ -336,7 +336,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { // synchronous. scheduleMicrotask(() { var exception = - new PostgreSQLException("Connection closed or query cancelled."); + new PostgreSQLException("Connection closed or query cancelled (reason: $error).", stackTrace: stackTrace); queries?.forEach((q) { q.completeError(exception); }); @@ -375,17 +375,17 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { } else { _transitionToState(_connectionState.onMessage(msg)); } - } catch (e) { - _handleSocketError(e); + } catch (e, st) { + _handleSocketError(e, st); } } } - void _handleSocketError(dynamic error) { + void _handleSocketError(Object error, StackTrace stack) { _connectionState = new _PostgreSQLConnectionStateClosed(); _socket.destroy(); - _cancelCurrentQueries(); + _cancelCurrentQueries(error, stack); } void _handleSocketClosed() { diff --git a/lib/src/query.dart b/lib/src/query.dart index 6bc87f4..a7ebda8 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -13,9 +13,9 @@ class Query { bool onlyReturnAffectedRowCount = false; String statementIdentifier; - Completer onComplete = new Completer.sync(); + Completer _onComplete = new Completer.sync(); - Future get future => onComplete.future; + Future get future => _onComplete.future; final String statement; final Map substitutionValues; @@ -142,15 +142,15 @@ class Query { void complete(int rowsAffected) { if (onlyReturnAffectedRowCount) { - onComplete.complete(rowsAffected); + _onComplete.complete(rowsAffected); return; } - onComplete.complete(rows.map((row) => row.toList()).toList()); + _onComplete.complete(rows.map((row) => row.toList()).toList()); } void completeError(dynamic error) { - onComplete.completeError(error); + _onComplete.completeError(error); } String toString() => statement; diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index 7aa408c..c9c589f 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -8,7 +8,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { beginQuery = new Query("BEGIN", {}, connection, this) ..onlyReturnAffectedRowCount = true; - beginQuery.onComplete.future + beginQuery.future .then(startTransaction) .catchError(handleTransactionQueryError); } diff --git a/pubspec.yaml b/pubspec.yaml index 747557f..7304156 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 0.9.5 +version: 0.9.6 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: @@ -9,7 +9,7 @@ environment: sdk: '>=1.19.0 <2.0.0' dependencies: - crypto: "^2.0.0" + crypto: ^2.0.0 dev_dependencies: test: '>=0.12.0 <0.13.0' From d01d1d3774d111fbf2dea561b100d435b4ae2dd6 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 5 Jul 2017 09:43:42 -0400 Subject: [PATCH 12/73] Adds cleanup method and API reference for notification (#19) * Cleanup method to centralize close down behavior * Add api ref * api ref * Move definition * Fix tests --- CHANGELOG.md | 1 + lib/src/connection.dart | 37 +++++++++++++++++++++++++++---- lib/src/server_messages.dart | 10 +-------- test/connection_test.dart | 42 ++++++++++++++++-------------------- test/notification_test.dart | 2 +- 5 files changed, 54 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d519c42..f65723b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.9.6 +- Adds `Connection.notifications` to listen for `NOTIFY` events. - Adds better error reporting. ## 0.9.5 diff --git a/lib/src/connection.dart b/lib/src/connection.dart index abdcf79..74177e8 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -80,7 +80,6 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { } final StreamController _notifications = new StreamController.broadcast(); - // Add flag for debugging that captures stack trace prior to execution /// Hostname of database this connection refers to. String host; @@ -109,7 +108,12 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// The processID of this backend. int processID; - /// The notifications from the database + /// Stream of notification from the database. + /// + /// Listen to this [Stream] to receive events from PostgreSQL NOTIFY commands. + /// + /// To determine whether or not the NOTIFY came from this instance, compare [processID] + /// to [Notification.processID]. Stream get notifications => _notifications.stream; /// Whether or not this connection is open or not. @@ -189,9 +193,9 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { await _socket?.close(); - await _notifications.close(); - _cancelCurrentQueries(); + + return _cleanup(); } /// Executes a query on this connection. @@ -304,6 +308,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _socket?.destroy(); _cancelCurrentQueries(); + _cleanup(); throw new PostgreSQLException( "Timed out trying to connect to database postgres://$host:$port/$databaseName."); } @@ -328,6 +333,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { } void _cancelCurrentQueries([Object error, StackTrace stackTrace]) { + error ??= "Cancelled"; var queries = _queryQueue; _queryQueue = []; @@ -386,12 +392,14 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _socket.destroy(); _cancelCurrentQueries(error, stack); + _cleanup(); } void _handleSocketClosed() { _connectionState = new _PostgreSQLConnectionStateClosed(); _cancelCurrentQueries(); + _cleanup(); } Future _upgradeSocketToSSL(Socket originalSocket, @@ -459,6 +467,10 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { return string; } + + Future _cleanup() async { + await _notifications.close(); + } } class _TransactionRollbackException implements Exception { @@ -466,3 +478,20 @@ class _TransactionRollbackException implements Exception { String reason; } + +/// Represents a notification from PostgreSQL. +/// +/// Instances of this type are created and sent via [PostgreSQLConnection.notifications]. +class Notification { + /// Creates an instance of this type. + Notification(this.processID, this.channel, this.payload); + + /// The backend ID from which the notification was generated. + final int processID; + + /// The name of the PostgreSQL channel that this notification occurred on. + final String channel; + + /// An optional data payload accompanying this notification. + final String payload; +} diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index d7ae66c..d40f529 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -282,12 +282,4 @@ class ErrorField { _buffer.writeCharCode(byte); } } -} - -class Notification { - final int processID; - final String channel; - final String payload; - - Notification(this.processID, this.channel, this.payload); -} +} \ No newline at end of file diff --git a/test/connection_test.dart b/test/connection_test.dart index 47efabe..9f670c3 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -95,22 +95,19 @@ void main() { username: "darttrust"); await conn.open(); + var errors = []; var futures = [ - conn.query("select 1", allowReuse: false), - conn.query("select 2", allowReuse: false), - conn.query("select 3", allowReuse: false), - conn.query("select 4", allowReuse: false), - conn.query("select 5", allowReuse: false) + conn.query("select 1", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 2", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 3", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 4", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 5", allowReuse: false).catchError((e) => errors.add(e)), ]; await conn.close(); - - try { - await Future.wait(futures); - expect(true, false); - } on PostgreSQLException catch (e) { - expect(e.message, contains("Connection closed")); - } + await Future.wait(futures); + expect(errors.length, 5); + expect(errors.map((e) => e.message), everyElement(contains("Connection closed"))); }); test( @@ -120,22 +117,19 @@ void main() { username: "darttrust", useSSL: true); await conn.open(); + var errors = []; var futures = [ - conn.query("select 1", allowReuse: false), - conn.query("select 2", allowReuse: false), - conn.query("select 3", allowReuse: false), - conn.query("select 4", allowReuse: false), - conn.query("select 5", allowReuse: false) + conn.query("select 1", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 2", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 3", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 4", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 5", allowReuse: false).catchError((e) => errors.add(e)), ]; await conn.close(); - - try { - await Future.wait(futures); - expect(true, false); - } on PostgreSQLException catch (e) { - expect(e.message, contains("Connection closed")); - } + await Future.wait(futures); + expect(errors.length, 5); + expect(errors.map((e) => e.message), everyElement(contains("Connection closed"))); }); }); diff --git a/test/notification_test.dart b/test/notification_test.dart index 1fd2df1..1aa6017 100644 --- a/test/notification_test.dart +++ b/test/notification_test.dart @@ -72,7 +72,7 @@ void main() { .timeout(new Duration(milliseconds: 200)); fail('There should be no notification'); - } on TimeoutException catch (e) {} + } on TimeoutException catch (_) {} }); test("Notification many channel", () async { From 0a195acf13d8e4c20bd5ef9f893b822d5cc90ce5 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 19 Jul 2017 11:10:26 -0400 Subject: [PATCH 13/73] Allows typecasting syntax (#20) * Cleanup method to centralize close down behavior * Add api ref * api ref * Move definition * Fix tests * Allows typecasting --- lib/src/query.dart | 18 ++++++++++++------ lib/src/substituter.dart | 4 ++++ test/interpolation_test.dart | 24 ++++++++++++++++++++++++ test/query_test.dart | 27 +++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/lib/src/query.dart b/lib/src/query.dart index a7ebda8..99c02a7 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -269,13 +269,18 @@ class PostgreSQLFormatIdentifier { } PostgreSQLFormatIdentifier(String t) { - var components = t.split(":"); - if (components.length == 1) { - name = components.first; - } else if (components.length == 2) { - name = components.first; + var components = t.split("::"); + if (components.length > 1) { + typeCast = components.sublist(1).join(""); + } + + var variableComponents = components.first.split(":"); + if (variableComponents.length == 1) { + name = variableComponents.first; + } else if (variableComponents.length == 2) { + name = variableComponents.first; - var dataTypeString = components.last; + var dataTypeString = variableComponents.last; if (dataTypeString != null) { typeCode = postgresCodeForDataTypeString(dataTypeString); } @@ -290,4 +295,5 @@ class PostgreSQLFormatIdentifier { String name; int typeCode; + String typeCast; } diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 2c55fa9..bcb3642 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -112,6 +112,10 @@ class PostgreSQLFormat { var val = replace(identifier, idx); idx++; + + if (identifier.typeCast != null) { + return val + "::" + identifier.typeCast; + } return val; } }).join(""); diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 1e1e500..a3d7126 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -34,6 +34,30 @@ void main() { expect(result, equals("122.0")); }); + test("Disambiguate PostgreSQL typecast", () { + var result = PostgreSQLFormat + .substitute("@id::jsonb", {"id": "12"}); + expect(result, "'12'::jsonb"); + }); + + test("PostgreSQL typecast appears in query", () { + var results = PostgreSQLFormat.substitute("SELECT * FROM t WHERE id=@id:int2 WHERE blob=@blob::jsonb AND blob='{\"a\":1}'::jsonb", { + "id": 2, + "blob": "{\"key\":\"value\"}" + }); + + expect(results, "SELECT * FROM t WHERE id=2 WHERE blob='{\"key\":\"value\"}'::jsonb AND blob='{\"a\":1}'::jsonb"); + }); + + test("Can both provide type and typecast", () { + var results = PostgreSQLFormat.substitute("SELECT * FROM t WHERE id=@id:int2::int4", { + "id": 2, + "blob": "{\"key\":\"value\"}" + }); + + expect(results, "SELECT * FROM t WHERE id=2::int4"); + }); + test("String identifiers get escaped", () { var result = PostgreSQLFormat .substitute("@id:text @foo", {"id": "1';select", "foo": "3\\4"}); diff --git a/test/query_test.dart b/test/query_test.dart index 1e37985..a9405ad 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -257,6 +257,19 @@ void main() { [0, 1] ]); }); + + test("Can cast text to int on db server", () async { + var results = await connection.query( + "INSERT INTO u (i1, i2) VALUES (@i1::int4, @i2::int4) RETURNING i1, i2", + substitutionValues: { + "i1": "0", + "i2": "1" + }); + + expect(results, [ + [0, 1] + ]); + }); }); group("Unsuccesful queries", () { @@ -308,6 +321,20 @@ void main() { contains("Format string specified identifier with name i1")); } }); + + test("Wrong type for parameter in substitution values fails", () async { + try { + await connection.query( + "INSERT INTO t (i1, i2) values (@i1:int4, @i2:int4)", + substitutionValues: { + "i1": "1", + "i2": 1 + }); + expect(true, false); + } on FormatException catch (e) { + expect(e.toString(), contains("Invalid type for parameter value")); + } + }); }); } From a65e856c630836990b9ee4dad635b2b75fb79bd9 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Thu, 28 Sep 2017 20:20:32 -0400 Subject: [PATCH 14/73] Adds JSONB column support (#24) * jsonb format * wip * Improving tests, fixing some bugs * update changelog --- CHANGELOG.md | 1 + lib/src/client_messages.dart | 1 + lib/src/postgresql_codec.dart | 41 ++++++++++++++++- lib/src/query.dart | 9 +++- lib/src/substituter.dart | 51 +++++++++++---------- test/decode_test.dart | 69 ++++++++++++++++++++-------- test/encoding_test.dart | 15 ++++++- test/json_test.dart | 85 +++++++++++++++++++++++++++++++++++ test/query_test.dart | 44 ++++++++++++++---- 9 files changed, 260 insertions(+), 56 deletions(-) create mode 100644 test/json_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f65723b..3cdad3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Adds `Connection.notifications` to listen for `NOTIFY` events. - Adds better error reporting. +- Adds support for JSONB columns. ## 0.9.5 diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 9b368ab..85b852f 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -295,6 +295,7 @@ class BindMessage extends ClientMessage { } else { buffer.setInt32(offset, p.length); offset += 4; + offset = p.bytes.fold(offset, (inOffset, byte) { buffer.setUint8(inOffset, byte); return inOffset + 1; diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart index dfef9b2..e4bdf6d 100644 --- a/lib/src/postgresql_codec.dart +++ b/lib/src/postgresql_codec.dart @@ -38,7 +38,12 @@ enum PostgreSQLDataType { timestampWithTimezone, /// Must be a [DateTime] (contains year, month and day only) - date + date, + + /// Must be encodable via [JSON.encode]. + /// + /// Values will be encoded via [JSON.encode] before being sent to the database. + json } /// A namespace for data encoding and decoding operations for PostgreSQL data. @@ -53,6 +58,7 @@ abstract class PostgreSQLCodec { static const int TypeDate = 1082; static const int TypeTimestamp = 1114; static const int TypeTimestampTZ = 1184; + static const int TypeJSONB = 3802; static String encode(dynamic value, {PostgreSQLDataType dataType: null, bool escapeStrings: true}) { @@ -83,6 +89,9 @@ abstract class PostgreSQLCodec { case PostgreSQLDataType.date: return encodeDateTime(value); + case PostgreSQLDataType.json: + return encodeJSON(value); + default: return encodeDefault(value, escapeStrings: escapeStrings); } @@ -196,6 +205,14 @@ abstract class PostgreSQLCodec { bd.setInt64( 0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); outBuffer = bd.buffer.asUint8List(); + } else if (postgresType == TypeJSONB) { + + var jsonBytes = UTF8.encode(JSON.encode(value)); + outBuffer = new Uint8List(jsonBytes.length + 1); + outBuffer[0] = 1; + for (var i = 0; i < jsonBytes.length; i++) { + outBuffer[i + 1] = jsonBytes[i]; + } } return outBuffer; @@ -328,6 +345,18 @@ abstract class PostgreSQLCodec { return "'$string'"; } + static String encodeJSON(dynamic value) { + if (value == null) { + return "null"; + } + + if (value is String) { + return "'${JSON.encode(value)}'"; + } + + return "${JSON.encode(value)}"; + } + static String encodeDefault(dynamic value, {bool escapeStrings: true}) { if (value == null) { return "null"; @@ -353,6 +382,10 @@ abstract class PostgreSQLCodec { return encodeBoolean(value); } + if (value is Map) { + return encodeJSON(value); + } + throw new PostgreSQLException( "Unknown inferred datatype from ${value.runtimeType}: $value"); } @@ -385,6 +418,12 @@ abstract class PostgreSQLCodec { return new DateTime.utc(2000) .add(new Duration(days: value.getInt32(0))); + case TypeJSONB: { + // Removes version which is first character and currently always '1' + var string = UTF8.decode(value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1)); + return JSON.decode(string); + } + default: return UTF8.decode( value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); diff --git a/lib/src/query.dart b/lib/src/query.dart index 99c02a7..151a46d 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -241,7 +241,7 @@ class FieldDescription { typedef String SQLReplaceIdentifierFunction( PostgreSQLFormatIdentifier identifier, int index); -enum PostgreSQLFormatTokenType { text, marker } +enum PostgreSQLFormatTokenType { text, variable } class PostgreSQLFormatToken { PostgreSQLFormatToken(this.type); @@ -261,7 +261,8 @@ class PostgreSQLFormatIdentifier { "boolean": PostgreSQLCodec.TypeBool, "date": PostgreSQLCodec.TypeDate, "timestamp": PostgreSQLCodec.TypeTimestamp, - "timestamptz": PostgreSQLCodec.TypeTimestampTZ + "timestamptz": PostgreSQLCodec.TypeTimestampTZ, + "jsonb": PostgreSQLCodec.TypeJSONB }; static int postgresCodeForDataTypeString(String dt) { @@ -283,6 +284,9 @@ class PostgreSQLFormatIdentifier { var dataTypeString = variableComponents.last; if (dataTypeString != null) { typeCode = postgresCodeForDataTypeString(dataTypeString); + if (typeCode == null) { + throw new FormatException("Invalid type code in substitution variable '$t'"); + } } } else { throw new FormatException( @@ -291,6 +295,7 @@ class PostgreSQLFormatIdentifier { // Strip @ name = name.substring(1, name.length); + } String name; diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index bcb3642..4ddb3dc 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -38,6 +38,8 @@ class PostgreSQLFormat { return "timestamptz"; case PostgreSQLDataType.date: return "date"; + case PostgreSQLDataType.json: + return "jsonb"; } return null; @@ -49,48 +51,48 @@ class PostgreSQLFormat { replace ??= (spec, index) => PostgreSQLCodec.encode(values[spec.name]); var items = []; - PostgreSQLFormatToken lastPtr = null; + PostgreSQLFormatToken currentPtr = null; var iterator = new RuneIterator(fmtString); iterator.moveNext(); while (iterator.current != null) { - if (lastPtr == null) { + if (currentPtr == null) { if (iterator.current == _AtSignCodeUnit) { - lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.marker); - lastPtr.buffer.writeCharCode(iterator.current); - items.add(lastPtr); + currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + currentPtr.buffer.writeCharCode(iterator.current); + items.add(currentPtr); } else { - lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); - lastPtr.buffer.writeCharCode(iterator.current); - items.add(lastPtr); + currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); + currentPtr.buffer.writeCharCode(iterator.current); + items.add(currentPtr); } - } else if (lastPtr.type == PostgreSQLFormatTokenType.text) { + } else if (currentPtr.type == PostgreSQLFormatTokenType.text) { if (iterator.current == _AtSignCodeUnit) { - lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.marker); - lastPtr.buffer.writeCharCode(iterator.current); - items.add(lastPtr); + currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + currentPtr.buffer.writeCharCode(iterator.current); + items.add(currentPtr); } else { - lastPtr.buffer.writeCharCode(iterator.current); + currentPtr.buffer.writeCharCode(iterator.current); } - } else if (lastPtr.type == PostgreSQLFormatTokenType.marker) { + } else if (currentPtr.type == PostgreSQLFormatTokenType.variable) { if (iterator.current == _AtSignCodeUnit) { iterator.movePrevious(); if (iterator.current == _AtSignCodeUnit) { - lastPtr.buffer.writeCharCode(iterator.current); - lastPtr.type = PostgreSQLFormatTokenType.text; + currentPtr.buffer.writeCharCode(iterator.current); + currentPtr.type = PostgreSQLFormatTokenType.text; } else { - lastPtr = - new PostgreSQLFormatToken(PostgreSQLFormatTokenType.marker); - lastPtr.buffer.writeCharCode(iterator.current); - items.add(lastPtr); + currentPtr = + new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + currentPtr.buffer.writeCharCode(iterator.current); + items.add(currentPtr); } iterator.moveNext(); } else if (_isIdentifier(iterator.current)) { - lastPtr.buffer.writeCharCode(iterator.current); + currentPtr.buffer.writeCharCode(iterator.current); } else { - lastPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); - lastPtr.buffer.writeCharCode(iterator.current); - items.add(lastPtr); + currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); + currentPtr.buffer.writeCharCode(iterator.current); + items.add(currentPtr); } } @@ -116,6 +118,7 @@ class PostgreSQLFormat { if (identifier.typeCast != null) { return val + "::" + identifier.typeCast; } + return val; } }).join(""); diff --git a/test/decode_test.dart b/test/decode_test.dart index e990e00..6ce63ce 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -4,16 +4,29 @@ import 'package:test/test.dart'; void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await connection.open(); - await connection.execute( - "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); - await connection.execute( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES (-2147483648, -9223372036854775808, TRUE, -32768, 'string', 10.0, 10.0, '1983-11-06', '1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000')"); - await connection.execute( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES (2147483647, 9223372036854775807, FALSE, 32767, 'a significantly longer string to the point where i doubt this actually matters', 10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', '2183-11-06 00:00:00.999999')"); + await connection.execute(""" + CREATE TEMPORARY TABLE t ( + i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, + t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb) + """); + + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) " + "VALUES (-2147483648, -9223372036854775808, TRUE, -32768, " + "'string', 10.0, 10.0, '1983-11-06', " + "'1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000', " + "'{\"key\":\"value\"}')"); + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) " + "VALUES (2147483647, 9223372036854775807, FALSE, 32767, " + "'a significantly longer string to the point where i doubt this actually matters', " + "10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', " + "'2183-11-06 00:00:00.999999', " + "'[{\"key\":1}]')"); + + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) " + "VALUES (null, null, null, null, null, null, null, null, null, null, null)"); }); tearDown(() async { await connection?.close(); @@ -24,6 +37,9 @@ void main() { var row1 = res[0]; var row2 = res[1]; + var row3 = res[2]; + + // lower bound row expect(row1[0], equals(-2147483648)); expect(row1[1], equals(1)); expect(row1[2], equals(-9223372036854775808)); @@ -38,17 +54,16 @@ void main() { expect(row1[9], equals(new DateTime.utc(1983, 11, 6))); expect(row1[10], equals(new DateTime.utc(1983, 11, 6, 6))); expect(row1[11], equals(new DateTime.utc(1983, 11, 6, 6))); + expect(row1[12], equals({"key": "value"})); + // upper bound row expect(row2[0], equals(2147483647)); expect(row2[1], equals(2)); expect(row2[2], equals(9223372036854775807)); expect(row2[3], equals(2)); expect(row2[4], equals(false)); expect(row2[5], equals(32767)); - expect( - row2[6], - equals( - "a significantly longer string to the point where i doubt this actually matters")); + expect(row2[6], equals("a significantly longer string to the point where i doubt this actually matters")); expect(row2[7] is double, true); expect(row2[7], equals(10.25)); expect(row2[8] is double, true); @@ -56,13 +71,32 @@ void main() { expect(row2[9], equals(new DateTime.utc(2183, 11, 6))); expect(row2[10], equals(new DateTime.utc(2183, 11, 6, 0, 0, 0, 111, 111))); expect(row2[11], equals(new DateTime.utc(2183, 11, 6, 0, 0, 0, 999, 999))); + expect( + row2[12], + equals([ + {"key": 1} + ])); + + // all null row + expect(row3[0], isNull); + expect(row3[1], equals(3)); + expect(row3[2], isNull); + expect(row3[3], equals(3)); + expect(row3[4], isNull); + expect(row3[5], isNull); + expect(row3[6], isNull); + expect(row3[7], isNull); + expect(row3[8], isNull); + expect(row3[9], isNull); + expect(row3[10], isNull); + expect(row3[11], isNull); + expect(row3[12], isNull); }); test("Fetch/insert empty string", () async { await connection.execute("CREATE TEMPORARY TABLE u (t text)"); - var results = await connection.query( - "INSERT INTO u (t) VALUES (@t:text) returning t", - substitutionValues: {"t": ""}); + var results = + await connection.query("INSERT INTO u (t) VALUES (@t:text) returning t", substitutionValues: {"t": ""}); expect(results, [ [""] ]); @@ -75,9 +109,8 @@ void main() { test("Fetch/insert null value", () async { await connection.execute("CREATE TEMPORARY TABLE u (t text)"); - var results = await connection.query( - "INSERT INTO u (t) VALUES (@t:text) returning t", - substitutionValues: {"t": null}); + var results = + await connection.query("INSERT INTO u (t) VALUES (@t:text) returning t", substitutionValues: {"t": null}); expect(results, [ [null] ]); diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 9a10464..be8ae70 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -173,8 +173,19 @@ void main() { "FALSE"); }); - test("UTF8String caches string regardless of which method is called first", - () { + + test("Encode JSONB", () { + expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.json), "null"); + expect(PostgreSQLCodec.encode("a", dataType: PostgreSQLDataType.json), "'\"a\"'"); + expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.json), "1"); + expect(PostgreSQLCodec.encode(2.0, dataType: PostgreSQLDataType.json), "2.0"); + expect(PostgreSQLCodec.encode({"a":"b"}, dataType: PostgreSQLDataType.json), "{\"a\":\"b\"}"); + expect(PostgreSQLCodec.encode([{"a":"b"}], dataType: PostgreSQLDataType.json), "[{\"a\":\"b\"}]"); + expect(PostgreSQLCodec.encode({"a":true}, dataType: PostgreSQLDataType.json), "{\"a\":true}"); + expect(PostgreSQLCodec.encode({"b":false}, dataType: PostgreSQLDataType.json), "{\"b\":false}"); + }); + + test("UTF8String caches string regardless of which method is called first", () { var u = new UTF8BackedString("abcd"); var v = new UTF8BackedString("abcd"); diff --git a/test/json_test.dart b/test/json_test.dart new file mode 100644 index 0000000..eaa7587 --- /dev/null +++ b/test/json_test.dart @@ -0,0 +1,85 @@ +import 'package:postgres/postgres.dart'; +import 'package:test/test.dart'; + +void main() { + PostgreSQLConnection connection; + + setUp(() async { + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + await connection.open(); + + await connection.execute(""" + CREATE TEMPORARY TABLE t (j jsonb) + """); + }); + + tearDown(() async { + await connection?.close(); + }); + + group("Storage", () { + test("Can store JSON String", () async { + var result = await connection.query("INSERT INTO t (j) VALUES ('\"xyz\"'::jsonb) RETURNING j"); + expect(result, [["xyz"]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [["xyz"]]); + }); + + test("Can store JSON String with driver type annotation", () async { + var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { + "a" : "xyz" + }); + expect(result, [["xyz"]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [["xyz"]]); + }); + + test("Can store JSON Number", () async { + var result = await connection.query("INSERT INTO t (j) VALUES ('4'::jsonb) RETURNING j"); + expect(result, [[4]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [[4]]); + }); + + test("Can store JSON Number with driver type annotation", () async { + var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { + "a": 4 + }); + expect(result, [[4]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [[4]]); + }); + + test("Can store JSON map", () async { + var result = await connection.query("INSERT INTO t (j) VALUES ('{\"a\":4}') RETURNING j"); + expect(result, [[{"a":4}]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [[{"a":4}]]); + }); + + test("Can store JSON map with driver type annotation", () async { + var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { + "a": {"a":4} + }); + expect(result, [[{"a":4}]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [[{"a":4}]]); + }); + + test("Can store JSON list", () async { + var result = await connection.query("INSERT INTO t (j) VALUES ('[{\"a\":4}]') RETURNING j"); + expect(result, [[[{"a":4}]]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [[[{"a":4}]]]); + }); + + test("Can store JSON list with driver type annotation", () async { + var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { + "a": [{"a":4}] + }); + expect(result, [[[{"a":4}]]]); + result = await connection.query("SELECT j FROM t"); + expect(result, [[[{"a":4}]]]); + }); + }); +} diff --git a/test/query_test.dart b/test/query_test.dart index a9405ad..1295879 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -12,7 +12,11 @@ void main() { username: "dart", password: "dart"); await connection.open(); await connection.execute( - "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + "CREATE TEMPORARY TABLE t " + "(i int, s serial, bi bigint, " + "bs bigserial, bl boolean, si smallint, " + "t text, f real, d double precision, " + "dt date, ts timestamp, tsz timestamptz, j jsonb)"); await connection.execute( "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); await connection @@ -67,7 +71,7 @@ void main() { test("Query without specifying types", () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) values " "(${PostgreSQLFormat.id("i")}," "${PostgreSQLFormat.id("bi")}," "${PostgreSQLFormat.id("bl")}," @@ -77,7 +81,8 @@ void main() { "${PostgreSQLFormat.id("d")}," "${PostgreSQLFormat.id("dt")}," "${PostgreSQLFormat.id("ts")}," - "${PostgreSQLFormat.id("tsz")}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz", + "${PostgreSQLFormat.id("tsz")}," + "${PostgreSQLFormat.id("j")}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j", substitutionValues: { "i": 1, "bi": 2, @@ -89,6 +94,7 @@ void main() { "dt": new DateTime.utc(2000), "ts": new DateTime.utc(2000, 2), "tsz": new DateTime.utc(2000, 3), + "j": {"a":"b"} }); var expectedRow = [ @@ -103,17 +109,18 @@ void main() { 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3) + new DateTime.utc(2000, 3), + {"a":"b"} ]; expect(result, [expectedRow]); result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j from t"); expect(result, [expectedRow]); }); test("Query by specifying all types", () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) values " "(${PostgreSQLFormat.id("i", type: PostgreSQLDataType.integer)}," "${PostgreSQLFormat.id("bi", type: PostgreSQLDataType.bigInteger)}," "${PostgreSQLFormat.id("bl", type: PostgreSQLDataType.boolean)}," @@ -123,7 +130,8 @@ void main() { "${PostgreSQLFormat.id("d", type: PostgreSQLDataType.double)}," "${PostgreSQLFormat.id("dt", type: PostgreSQLDataType.date)}," "${PostgreSQLFormat.id("ts", type: PostgreSQLDataType.timestampWithoutTimezone)}," - "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz", + "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}," + "${PostgreSQLFormat.id("j", type: PostgreSQLDataType.json)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j", substitutionValues: { "i": 1, "bi": 2, @@ -135,6 +143,7 @@ void main() { "dt": new DateTime.utc(2000), "ts": new DateTime.utc(2000, 2), "tsz": new DateTime.utc(2000, 3), + "j": {"key": "value"} }); var expectedRow = [ @@ -149,12 +158,13 @@ void main() { 6.0, new DateTime.utc(2000), new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3) + new DateTime.utc(2000, 3), + {"key": "value"} ]; expect(result, [expectedRow]); result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j from t"); expect(result, [expectedRow]); }); @@ -272,6 +282,7 @@ void main() { }); }); + group("Unsuccesful queries", () { var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); @@ -335,6 +346,21 @@ void main() { expect(e.toString(), contains("Invalid type for parameter value")); } }); + + test("Invalid type code", () async { + try { + await connection.query( + "INSERT INTO t (i1, i2) values (@i1:qwerty, @i2:int4)", + substitutionValues: { + "i1": "1", + "i2": 1 + }); + expect(true, false); + } on FormatException catch (e) { + expect(e.toString(), contains("Invalid type code")); + expect(e.toString(), contains("'@i1:qwerty")); + } + }); }); } From df132e1a5f99fdefc93607519fec2c02c9ebe696 Mon Sep 17 00:00:00 2001 From: AndrewSt Date: Thu, 5 Oct 2017 19:42:19 +0300 Subject: [PATCH 15/73] fix encode UTF16 symbols with escape characters (#26) * fix encode UTF16 symbols with escape characters * rename tests in query_test.dart --- lib/src/postgresql_codec.dart | 2 +- test/interpolation_test.dart | 18 +++++++++++++++ test/query_test.dart | 41 +++++++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart index e4bdf6d..22ea1a4 100644 --- a/lib/src/postgresql_codec.dart +++ b/lib/src/postgresql_codec.dart @@ -248,7 +248,7 @@ abstract class PostgreSQLCodec { if (quoteCount == 0 && backslashCount == 0) { buf.write(text); } else { - UTF8.encode(text).forEach((i) { + text.codeUnits.forEach((i) { if (i == quoteCodeUnit || i == backslashCodeUnit) { buf.writeCharCode(i); buf.writeCharCode(i); diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index a3d7126..a134588 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -58,6 +58,24 @@ void main() { expect(results, "SELECT * FROM t WHERE id=2::int4"); }); + test("UTF16 symbols with quotes", () { + var value = "'©™®'"; + var results = PostgreSQLFormat.substitute("INSERT INTO t (t) VALUES (@t)", { + "t": value + }); + + expect(results, "INSERT INTO t (t) VALUES ('''©™®''')"); + }); + + test("UTF16 symbols with backslash", () { + var value = "'©\\™®'"; + var results = PostgreSQLFormat.substitute("INSERT INTO t (t) VALUES (@t)", { + "t": value + }); + + expect(results, "INSERT INTO t (t) VALUES ( E'''©\\\\™®''')"); + }); + test("String identifiers get escaped", () { var result = PostgreSQLFormat .substitute("@id:text @foo", {"id": "1';select", "foo": "3\\4"}); diff --git a/test/query_test.dart b/test/query_test.dart index 1295879..f90943e 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -27,7 +27,7 @@ void main() { await connection.close(); }); - test("UTF8 strings in value", () async { + test("UTF16 strings in value", () async { var result = await connection.query( "INSERT INTO t (t) values " "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})" @@ -43,7 +43,7 @@ void main() { expect(result, [expectedRow]); }); - test("UTF8 strings in query", () async { + test("UTF16 strings in query", () async { var result = await connection.query("INSERT INTO t (t) values ('°∆') RETURNING t"); @@ -54,6 +54,43 @@ void main() { expect(result, [expectedRow]); }); + test("UTF16 strings in value with escape characters", () async { + await connection.execute( + "INSERT INTO t (t) values " + "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", + substitutionValues: { + "t": "'©™®'", + }); + + var expectedRow = ["'©™®'"]; + + var result = await connection.query("select t from t"); + expect(result, [expectedRow]); + }); + + test("UTF16 strings in value with backslash", () async { + await connection.execute( + "INSERT INTO t (t) values " + "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", + substitutionValues: { + "t": "°\\'©™®'", + }); + + var expectedRow = ["°\\'©™®'"]; + + var result = await connection.query("select t from t"); + expect(result, [expectedRow]); + }); + + test("UTF16 strings in query with escape characters", () async { + await connection.execute("INSERT INTO t (t) values ('°''©™®''')"); + + var expectedRow = ["°'©™®'"]; + + var result = await connection.query("select t from t"); + expect(result, [expectedRow]); + }); + test("Really long raw substitution value", () async { var result = await connection.query( "INSERT INTO t (t) VALUES (${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)}) returning t;", From 62403dcc3d09d1423049c93c3316ab8469266d0d Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Thu, 5 Oct 2017 12:44:05 -0400 Subject: [PATCH 16/73] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cdad3a..4a11442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## 0.9.6 -- Adds `Connection.notifications` to listen for `NOTIFY` events. +- Adds `Connection.notifications` to listen for `NOTIFY` events (thanks @andrewst) - Adds better error reporting. - Adds support for JSONB columns. +- Fixes issue when encoding UTF16 characters (thanks @andrewst) ## 0.9.5 From edb816caf40f3a0cdb2425de771c0c95f0c5e9cf Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 19 Mar 2018 09:52:37 -0400 Subject: [PATCH 17/73] Adds PostgreSQLConnection.mappedResultsQuery (#27) * Adds PostgreSQLConnection.mappedResultsQuery * Add API ref note * Added better handler for query preprocessing exceptions in transactions --- CHANGELOG.md | 4 + README.md | 21 +++- lib/src/connection.dart | 192 +++++++++++++++++++++++---------- lib/src/connection_fsm.dart | 82 ++++++-------- lib/src/query.dart | 58 ++++------ lib/src/transaction_proxy.dart | 21 ++-- pubspec.yaml | 2 +- test/map_return_test.dart | 140 ++++++++++++++++++++++++ test/transaction_test.dart | 19 ++++ 9 files changed, 382 insertions(+), 157 deletions(-) create mode 100644 test/map_return_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a11442..acff220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.7 + +- Adds `Connection.mappedResultsQuery` to return query results as a `Map` with keys for table and column names. + ## 0.9.6 - Adds `Connection.notifications` to listen for `NOTIFY` events (thanks @andrewst) diff --git a/README.md b/README.md index af132f5..263282c 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,28 @@ await connection.open(); Execute queries with `query`: ```dart -var results = await connection.query("SELECT a, b FROM table WHERE a = @aValue", substitutionValues: { +List> results = await connection.query("SELECT a, b FROM table WHERE a = @aValue", substitutionValues: { "aValue" : 3 }); + +for (final row in results) { + var a = row[0]; + var b = row[1]; + +} +``` + +Return rows as maps containing table and column names: + +```dart +List>> results = await connection.mappedResultsQuery( + "SELECT t.id, t.name, u.name FROM t LEFT OUTER JOIN u ON t.id=u.t_id"); + +for (final row in results) { + var tID = row["t"]["id"]; + var tName = row["t"]["name"]; + var uName = row["u"]["name"]; +} ``` Execute queries in a transaction: diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 74177e8..1f44ab4 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -11,7 +11,9 @@ import 'dart:io'; import 'client_messages.dart'; part 'connection_fsm.dart'; + part 'transaction_proxy.dart'; + part 'exceptions.dart'; abstract class PostgreSQLExecutionContext { @@ -45,8 +47,7 @@ abstract class PostgreSQLExecutionContext { /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. - Future execute(String fmtString, - {Map substitutionValues: null}); + Future execute(String fmtString, {Map substitutionValues: null}); /// Cancels a transaction on this context. /// @@ -70,11 +71,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// [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: null, - this.password: null, - this.timeoutInSeconds: 30, - this.timeZone: "UTC", - this.useSSL: false}) { + {this.username: null, this.password: null, this.timeoutInSeconds: 30, this.timeZone: "UTC", this.useSSL: false}) { _connectionState = new _PostgreSQLConnectionStateClosed(); _connectionState.connection = this; } @@ -135,15 +132,17 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { Map _reuseMap = {}; int _reuseCounter = 0; + Map _tableOIDNameMap = {}; + int _secretKey; List _salt; bool _hasConnectedPreviously = false; _PostgreSQLConnectionState _connectionState; - List _queryQueue = []; + List> _queryQueue = []; - Query get _pendingQuery { + Query get _pendingQuery { if (_queryQueue.isEmpty) { return null; } @@ -159,14 +158,11 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// 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 { if (_hasConnectedPreviously) { - throw new PostgreSQLException( - "Attempting to reopen a closed connection. Create a new instance instead."); + throw new PostgreSQLException("Attempting to reopen a closed connection. Create a new instance instead."); } _hasConnectedPreviously = true; - _socket = await Socket - .connect(host, port) - .timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); + _socket = await Socket.connect(host, port).timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); _framer = new MessageFramer(); if (useSSL) { @@ -175,14 +171,11 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { var connectionComplete = new Completer(); - _socket.listen(_readData, - onError: _handleSocketError, onDone: _handleSocketClosed); + _socket.listen(_readData, onError: _handleSocketError, onDone: _handleSocketClosed); - _transitionToState( - new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); + _transitionToState(new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); - return connectionComplete.future - .timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); + return connectionComplete.future.timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); } /// Closes a connection. @@ -220,20 +213,64 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. /// Future>> query(String fmtString, - {Map substitutionValues: null, - bool allowReuse: true}) async { + {Map substitutionValues: null, bool allowReuse: true}) async { + if (isClosed) { + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + } + + var query = new Query>>(fmtString, substitutionValues, this, null); + if (allowReuse) { + query.statementIdentifier = _reuseIdentifierForQuery(query); + } + + final rows = await _enqueue(query); + return rows.map((Iterable row) => row.toList()).toList(); + } + + /// Executes a query on this connection and returns each row as a [Map]. + /// + /// This method constructs and executes a query in the same way as [query], but returns each row as a [Map]. + /// + /// (Note: this method will execute additional queries to resolve table names the first time a table is encountered. These table names are cached per instance of this type.) + /// + /// Each row map contains key-value pairs for every table in the query. The value is a [Map] that contains + /// key-value pairs for each column from that table. For example, consider + /// the following query: + /// + /// SELECT employee.id, employee.name FROM employee; + /// + /// This method would return the following structure: + /// + /// [ + /// {"employee" : {"name": "Bob", "id": 1}} + /// ] + /// + /// The purpose of this nested structure is to disambiguate columns that have the same name in different tables. For example, consider a query with a SQL JOIN: + /// + /// SELECT employee.id, employee.name, company.name FROM employee LEFT OUTER JOIN company ON employee.company_id=company.id; + /// + /// Each returned [Map] would contain `employee` and `company` keys. The `name` key would be present in both inner maps. + /// + /// [ + /// { + /// "employee": {"name": "Bob", "id": 1}, + /// "company: {"name": "stable|kernel"} + /// } + /// ] + Future>>> mappedResultsQuery(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}) async { if (isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } - var query = new Query>>( - fmtString, substitutionValues, this, null); + var query = new Query>>(fmtString, substitutionValues, this, null); if (allowReuse) { query.statementIdentifier = _reuseIdentifierForQuery(query); } - return await _enqueue(query); + final rows = await _enqueue(query); + + return _mapifyRows(rows, query.fieldDescriptions); } /// Executes a query on this connection. @@ -243,17 +280,15 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. - Future execute(String fmtString, - {Map substitutionValues: null}) async { + Future execute(String fmtString, {Map substitutionValues: null}) { if (isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } var query = new Query(fmtString, substitutionValues, this, null) ..onlyReturnAffectedRowCount = true; - return await _enqueue(query); + return _enqueue(query); } /// Executes a series of queries inside a transaction on this connection. @@ -283,11 +318,9 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// ctx.query("INSERT INTO t (id) VALUES (2)"); /// } /// }); - Future transaction( - Future queryBlock(PostgreSQLExecutionContext connection)) async { + Future transaction(Future queryBlock(PostgreSQLExecutionContext connection)) async { if (isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } var proxy = new _TransactionProxy(this, queryBlock); @@ -303,24 +336,67 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { //////// + Future>>> _mapifyRows( + List> rows, List columns) async { + //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. + // It's not a significant impact here, but an area for optimization. This includes + // assigning resolvedTableName + final tableOIDs = new Set.from(columns.map((f) => f.tableID)); + final unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList() + ..sort((int lhs, int rhs) => lhs.compareTo(rhs)); + + if (unresolvedTableOIDs.isNotEmpty) { + await _resolveTableOIDs(unresolvedTableOIDs); + } + + columns.forEach((desc) { + desc.resolvedTableName = _tableOIDNameMap[desc.tableID]; + }); + + final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); + return rows.map((row) { + var rowMap = new Map.fromIterable(tableNames, key: (name) => name, value: (_) => {}); + + final iterator = columns.iterator; + row.forEach((column) { + iterator.moveNext(); + rowMap[iterator.current.resolvedTableName][iterator.current.fieldName] = column; + }); + + return rowMap; + }).toList(); + } + + Future _resolveTableOIDs(List oids) async { + final unresolvedIDString = oids.join(","); + final orderedTableNames = await query( + "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + + final iterator = oids.iterator; + orderedTableNames.forEach((tableName) { + iterator.moveNext(); + if (tableName.first != null) { + _tableOIDNameMap[iterator.current] = tableName.first; + } + }); + } + void _timeout() { _connectionState = new _PostgreSQLConnectionStateClosed(); _socket?.destroy(); _cancelCurrentQueries(); _cleanup(); - throw new PostgreSQLException( - "Timed out trying to connect to database postgres://$host:$port/$databaseName."); + throw new PostgreSQLException("Timed out trying to connect to database postgres://$host:$port/$databaseName."); } - Future _enqueue(Query query) async { + Future _enqueue(Query query) async { _queryQueue.add(query); _transitionToState(_connectionState.awake()); var result = null; try { result = await query.future; - _cacheQuery(query); _queryQueue.remove(query); } catch (e) { @@ -376,8 +452,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { if (msg is ErrorResponseMessage) { _transitionToState(_connectionState.onErrorResponse(msg)); } else if (msg is NotificationResponseMessage) { - _notifications.add( - new Notification(msg.processID, msg.channel, msg.payload)); + _notifications.add(new Notification(msg.processID, msg.channel, msg.payload)); } else { _transitionToState(_connectionState.onMessage(msg)); } @@ -402,21 +477,21 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _cleanup(); } - Future _upgradeSocketToSSL(Socket originalSocket, - {int timeout: 30}) async { + Future _upgradeSocketToSSL(Socket originalSocket, {int timeout: 30}) async { var sslCompleter = new Completer(); - originalSocket.listen((data) { - if (data.length != 1) { - sslCompleter.completeError(new PostgreSQLException( - "Could not initalize SSL connection, received unknown byte stream.")); - return; - } - - sslCompleter.complete(data.first); - }, - onDone: () => sslCompleter.completeError(new PostgreSQLException( - "Could not initialize SSL connection, connection closed during handshake.")), + originalSocket.listen( + (data) { + if (data.length != 1) { + sslCompleter.completeError( + new PostgreSQLException("Could not initalize SSL connection, received unknown byte stream.")); + return; + } + + sslCompleter.complete(data.first); + }, + onDone: () => sslCompleter.completeError( + new PostgreSQLException("Could not initialize SSL connection, connection closed during handshake.")), onError: (err) { sslCompleter.completeError(err); }); @@ -426,8 +501,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { byteBuffer.setUint32(4, 80877103); originalSocket.add(byteBuffer.buffer.asUint8List()); - var responseByte = await sslCompleter.future - .timeout(new Duration(seconds: timeout), onTimeout: _timeout); + var responseByte = await sslCompleter.future.timeout(new Duration(seconds: timeout), onTimeout: _timeout); if (responseByte == 83) { return SecureSocket .secure(originalSocket, onBadCertificate: (certificate) => true) @@ -437,7 +511,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { throw new PostgreSQLException("SSL not allowed for this connection."); } - void _cacheQuery(Query query) { + void _cacheQuery(Query query) { if (query.cache == null) { return; } @@ -455,7 +529,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { return _reuseMap[statementIdentifier]; } - String _reuseIdentifierForQuery(Query q) { + String _reuseIdentifierForQuery(Query q) { var existing = _reuseMap[q.statement]; if (existing != null) { return existing.preparedStatementName; diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 9eef54c..ff9426d 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -18,8 +18,7 @@ abstract class _PostgreSQLConnectionState { _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); - if (exception.severity == PostgreSQLSeverity.fatal || - exception.severity == PostgreSQLSeverity.panic) { + if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { return new _PostgreSQLConnectionStateClosed(); } @@ -39,16 +38,14 @@ class _PostgreSQLConnectionStateClosed extends _PostgreSQLConnectionState {} Socket connected, prior to any PostgreSQL handshaking - initiates that handshaking */ -class _PostgreSQLConnectionStateSocketConnected - extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateSocketConnected(this.completer); Completer completer; _PostgreSQLConnectionState onEnter() { - var startupMessage = new StartupMessage( - connection.databaseName, connection.timeZone, - username: connection.username); + var startupMessage = + new StartupMessage(connection.databaseName, connection.timeZone, username: connection.username); connection._socket.add(startupMessage.asBytes()); @@ -75,9 +72,8 @@ class _PostgreSQLConnectionStateSocketConnected return new _PostgreSQLConnectionStateAuthenticating(completer); } - completer.completeError( - new PostgreSQLException("Unsupported authentication type ${authMessage - .type}, closing connection.")); + completer.completeError(new PostgreSQLException("Unsupported authentication type ${authMessage + .type}, closing connection.")); return new _PostgreSQLConnectionStateClosed(); } @@ -87,15 +83,13 @@ class _PostgreSQLConnectionStateSocketConnected Authenticating state */ -class _PostgreSQLConnectionStateAuthenticating - extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateAuthenticating extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateAuthenticating(this.completer); Completer completer; _PostgreSQLConnectionState onEnter() { - var authMessage = new AuthMD5Message( - connection.username, connection.password, connection._salt); + var authMessage = new AuthMD5Message(connection.username, connection.password, connection._salt); connection._socket.add(authMessage.asBytes()); @@ -130,8 +124,7 @@ class _PostgreSQLConnectionStateAuthenticating Authenticated state */ -class _PostgreSQLConnectionStateAuthenticated - extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateAuthenticated extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateAuthenticated(this.completer); Completer completer; @@ -178,7 +171,7 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { return this; } - _PostgreSQLConnectionState processQuery(Query q) { + _PostgreSQLConnectionState processQuery(Query q) { try { if (q.onlyReturnAffectedRowCount) { q.sendSimple(connection._socket); @@ -217,7 +210,7 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateBusy(this.query); - Query query; + Query query; PostgreSQLException returningException = null; int rowsAffected = 0; @@ -228,8 +221,7 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { var exception = new PostgreSQLException._(message.fields); returningException ??= exception; - if (exception.severity == PostgreSQLSeverity.fatal || - exception.severity == PostgreSQLSeverity.panic) { + if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { return new _PostgreSQLConnectionStateClosed(); } @@ -243,29 +235,23 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { //print("(${query.statement}) -> $message"); if (message is ReadyForQueryMessage) { - if (message.state == ReadyForQueryMessage.StateIdle) { - if (returningException != null) { - query.completeError(returningException); - } else { - query.complete(rowsAffected); - } - - return new _PostgreSQLConnectionStateIdle(); - } else if (message.state == ReadyForQueryMessage.StateTransaction) { - if (returningException != null) { - query.completeError(returningException); - } else { - query.complete(rowsAffected); - } - - return new _PostgreSQLConnectionStateReadyInTransaction( - query.transaction); - } else if (message.state == ReadyForQueryMessage.StateTransactionError) { + if (message.state == ReadyForQueryMessage.StateTransactionError) { // This should cancel the transaction, we may have to send a commit here query.completeError(returningException); - return new _PostgreSQLConnectionStateTransactionFailure( - query.transaction); + return new _PostgreSQLConnectionStateTransactionFailure(query.transaction); + } + + if (returningException != null) { + query.completeError(returningException); + } else { + query.complete(rowsAffected); } + + if (message.state == ReadyForQueryMessage.StateTransaction) { + return new _PostgreSQLConnectionStateReadyInTransaction(query.transaction); + } + + return new _PostgreSQLConnectionStateIdle(); } else if (message is CommandCompleteMessage) { rowsAffected = message.rowsAffected; } else if (message is RowDescriptionMessage) { @@ -273,8 +259,7 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } else if (message is DataRowMessage) { query.addRow(message.values); } else if (message is ParameterDescriptionMessage) { - var validationException = - query.validateParameters(message.parameterTypeIDs); + var validationException = query.validateParameters(message.parameterTypeIDs); if (validationException != null) { query.cache = null; } @@ -287,8 +272,7 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { /* Idle Transaction State */ -class _PostgreSQLConnectionStateReadyInTransaction - extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateReadyInTransaction(this.transaction); _TransactionProxy transaction; @@ -306,7 +290,7 @@ class _PostgreSQLConnectionStateReadyInTransaction return this; } - _PostgreSQLConnectionState processQuery(Query q) { + _PostgreSQLConnectionState processQuery(Query q) { try { if (q.onlyReturnAffectedRowCount) { q.sendSimple(connection._socket); @@ -320,7 +304,7 @@ class _PostgreSQLConnectionStateReadyInTransaction } catch (e) { scheduleMicrotask(() { q.completeError(e); - connection._transitionToState(new _PostgreSQLConnectionStateIdle()); + connection._transitionToState(new _PostgreSQLConnectionStateTransactionFailure(transaction)); }); return new _PostgreSQLConnectionStateDeferredFailure(); @@ -332,8 +316,7 @@ class _PostgreSQLConnectionStateReadyInTransaction Transaction error state */ -class _PostgreSQLConnectionStateTransactionFailure - extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateTransactionFailure extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateTransactionFailure(this.transaction); _TransactionProxy transaction; @@ -347,5 +330,4 @@ class _PostgreSQLConnectionStateTransactionFailure Hack for deferred error */ -class _PostgreSQLConnectionStateDeferredFailure - extends _PostgreSQLConnectionState {} +class _PostgreSQLConnectionStateDeferredFailure extends _PostgreSQLConnectionState {} diff --git a/lib/src/query.dart b/lib/src/query.dart index 151a46d..c9ecbdf 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -8,10 +8,10 @@ import 'dart:typed_data'; import 'dart:convert'; class Query { - Query(this.statement, this.substitutionValues, this.connection, - this.transaction); + Query(this.statement, this.substitutionValues, this.connection, this.transaction); bool onlyReturnAffectedRowCount = false; + String statementIdentifier; Completer _onComplete = new Completer.sync(); @@ -25,6 +25,7 @@ class Query { List specifiedParameterTypeCodes; List _fieldDescriptions; + List get fieldDescriptions => _fieldDescriptions; void set fieldDescriptions(List fds) { @@ -32,7 +33,7 @@ class Query { cache?.fieldDescriptions = fds; } - List> rows = []; + List rows = []; QueryCache cache; @@ -60,12 +61,9 @@ class Query { return "\$$index"; }); - specifiedParameterTypeCodes = - formatIdentifiers.map((i) => i.typeCode).toList(); + specifiedParameterTypeCodes = formatIdentifiers.map((i) => i.typeCode).toList(); - var parameterList = formatIdentifiers - .map((id) => encodeParameter(id, substitutionValues)) - .toList(); + var parameterList = formatIdentifiers.map((id) => encodeParameter(id, substitutionValues)).toList(); var messages = [ new ParseMessage(sqlString, statementName: statementName), @@ -82,27 +80,20 @@ class Query { socket.add(ClientMessage.aggregateBytes(messages)); } - void sendCachedQuery(Socket socket, QueryCache cacheQuery, - Map substitutionValues) { + void sendCachedQuery(Socket socket, QueryCache cacheQuery, Map substitutionValues) { var statementName = cacheQuery.preparedStatementName; - var parameterList = cacheQuery.orderedParameters - .map((identifier) => encodeParameter(identifier, substitutionValues)) - .toList(); + var parameterList = + cacheQuery.orderedParameters.map((identifier) => encodeParameter(identifier, substitutionValues)).toList(); - var bytes = ClientMessage.aggregateBytes([ - new BindMessage(parameterList, statementName: statementName), - new ExecuteMessage(), - new SyncMessage() - ]); + var bytes = ClientMessage.aggregateBytes( + [new BindMessage(parameterList, statementName: statementName), new ExecuteMessage(), new SyncMessage()]); socket.add(bytes); } - ParameterValue encodeParameter(PostgreSQLFormatIdentifier identifier, - Map substitutionValues) { + ParameterValue encodeParameter(PostgreSQLFormatIdentifier identifier, Map substitutionValues) { if (identifier.typeCode != null) { - return new ParameterValue.binary( - substitutionValues[identifier.name], identifier.typeCode); + return new ParameterValue.binary(substitutionValues[identifier.name], identifier.typeCode); } else { return new ParameterValue.text(substitutionValues[identifier.name]); } @@ -110,11 +101,9 @@ class Query { PostgreSQLException validateParameters(List parameterTypeIDs) { var actualParameterTypeCodeIterator = parameterTypeIDs.iterator; - var parametersAreMismatched = - specifiedParameterTypeCodes.map((specifiedTypeCode) { + var parametersAreMismatched = specifiedParameterTypeCodes.map((specifiedTypeCode) { actualParameterTypeCodeIterator.moveNext(); - return actualParameterTypeCodeIterator.current == - (specifiedTypeCode ?? actualParameterTypeCodeIterator.current); + return actualParameterTypeCodeIterator.current == (specifiedTypeCode ?? actualParameterTypeCodeIterator.current); }).any((v) => v == false); if (parametersAreMismatched) { @@ -146,7 +135,7 @@ class Query { return; } - _onComplete.complete(rows.map((row) => row.toList()).toList()); + _onComplete.complete(rows); } void completeError(dynamic error) { @@ -164,19 +153,14 @@ class QueryCache { List fieldDescriptions; bool get isValid { - return preparedStatementName != null && - orderedParameters != null && - fieldDescriptions != null; + return preparedStatementName != null && orderedParameters != null && fieldDescriptions != null; } } class ParameterValue { ParameterValue.binary(dynamic value, this.postgresType) { isBinary = true; - bytes = PostgreSQLCodec - .encodeBinary(value, this.postgresType) - ?.buffer - ?.asUint8List(); + bytes = PostgreSQLCodec.encodeBinary(value, this.postgresType)?.buffer?.asUint8List(); length = bytes?.length ?? 0; } @@ -203,6 +187,8 @@ class FieldDescription { int typeModifier; int formatCode; + String resolvedTableName; + int parse(ByteData byteData, int initialOffset) { var offset = initialOffset; var buf = new StringBuffer(); @@ -238,8 +224,7 @@ class FieldDescription { } } -typedef String SQLReplaceIdentifierFunction( - PostgreSQLFormatIdentifier identifier, int index); +typedef String SQLReplaceIdentifierFunction(PostgreSQLFormatIdentifier identifier, int index); enum PostgreSQLFormatTokenType { text, variable } @@ -295,7 +280,6 @@ class PostgreSQLFormatIdentifier { // Strip @ name = name.substring(1, name.length); - } String name; diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index c9c589f..b342e2f 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -13,12 +13,12 @@ class _TransactionProxy implements PostgreSQLExecutionContext { .catchError(handleTransactionQueryError); } - Query beginQuery; + Query beginQuery; Completer completer = new Completer(); Future get future => completer.future; - Query get pendingQuery { + Query get pendingQuery { if (queryQueue.length > 0) { return queryQueue.first; } @@ -26,7 +26,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { return null; } - List queryQueue = []; + List> queryQueue = []; PostgreSQLConnection connection; _TransactionQuerySignature executionBlock; @@ -49,7 +49,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { query.statementIdentifier = connection._reuseIdentifierForQuery(query); } - return await enqueue(query); + return enqueue(query); } Future execute(String fmtString, @@ -91,9 +91,10 @@ class _TransactionProxy implements PostgreSQLExecutionContext { completer.complete(result); } - Future handleTransactionQueryError(dynamic err) async {} + Future handleTransactionQueryError(dynamic err) async { + } - Future enqueue(Query query) async { + Future enqueue(Query query) async { queryQueue.add(query); connection._transitionToState(connection._connectionState.awake()); @@ -104,9 +105,11 @@ class _TransactionProxy implements PostgreSQLExecutionContext { connection._cacheQuery(query); queryQueue.remove(query); } catch (e) { - connection._cacheQuery(query); - queryQueue.remove(query); - rethrow; + queryQueue = []; + + await execute("ROLLBACK"); + completer.completeError(e); + return null; } return result; diff --git a/pubspec.yaml b/pubspec.yaml index 7304156..ddb08af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 0.9.6 +version: 0.9.7 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: diff --git a/test/map_return_test.dart b/test/map_return_test.dart new file mode 100644 index 0000000..68489eb --- /dev/null +++ b/test/map_return_test.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'package:postgres/postgres.dart'; +import 'package:test/test.dart'; + +void main() { + InterceptingConnection connection; + + setUp(() async { + connection = new InterceptingConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + await connection.open(); + + await connection.execute(""" + CREATE TEMPORARY TABLE t (id int primary key, name text) + """); + + await connection.execute(""" + CREATE TEMPORARY TABLE u (id int primary key, name text, t_id int references t (id)) + """); + + await connection.execute("INSERT INTO t (id, name) VALUES (1, 'a')"); + await connection.execute("INSERT INTO t (id, name) VALUES (2, 'b')"); + await connection.execute("INSERT INTO t (id, name) VALUES (3, 'c')"); + await connection.execute("INSERT INTO u (id, name, t_id) VALUES (1, 'ua', 1)"); + await connection.execute("INSERT INTO u (id, name, t_id) VALUES (2, 'ub', 1)"); + await connection.execute("INSERT INTO u (id, name, t_id) VALUES (3, 'uc', 2)"); + }); + + tearDown(() async { + await connection?.close(); + }); + + test("Get row map without specifying columns", () async { + final results = await connection.mappedResultsQuery("SELECT * from t ORDER BY id ASC"); + expect(results, [ + { + "t": {"id": 1, "name": "a"} + }, + { + "t": {"id": 2, "name": "b"} + }, + { + "t": {"id": 3, "name": "c"} + }, + ]); + }); + + test("Get row map by with specified columns", () async { + final results = await connection.mappedResultsQuery("SELECT name, id from t ORDER BY id ASC"); + expect(results, [ + { + "t": {"id": 1, "name": "a"} + }, + { + "t": {"id": 2, "name": "b"} + }, + { + "t": {"id": 3, "name": "c"} + }, + ]); + + final nextResults = await connection.mappedResultsQuery("SELECT name from t ORDER BY name DESC"); + expect(nextResults, [ + { + "t": {"name": "c"} + }, + { + "t": {"name": "b"} + }, + { + "t": {"name": "a"} + }, + ]); + }); + + test("Get row with joined row", () async { + final results = await connection.mappedResultsQuery( + "SELECT t.name, t.id, u.id, u.name, u.t_id from t LEFT OUTER JOIN u ON t.id=u.t_id ORDER BY t.id ASC"); + expect(results, [ + { + "t": {"name": "a", "id": 1}, + "u": {"id": 1, "name": "ua", "t_id": 1} + }, + { + "t": {"name": "a", "id": 1}, + "u": {"id": 2, "name": "ub", "t_id": 1} + }, + { + "t": {"name": "b", "id": 2}, + "u": {"id": 3, "name": "uc", "t_id": 2} + }, + { + "t": {"name": "c", "id": 3}, + "u": {"name": null, "id": null, "t_id": null} + } + ]); + }); + + test("Table names get cached", () async { + final regex = new RegExp("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN \\(([0-9]*)\\) ORDER BY oid ASC"); + final oids = []; + + await connection.mappedResultsQuery("SELECT id FROM t"); + expect(connection.queries.length, 1); + var match = regex.firstMatch(connection.queries.first); + oids.add(match.group(1)); + connection.queries.clear(); + + await connection.mappedResultsQuery("SELECT id FROM t"); + expect(connection.queries.length, 0); + + await connection.mappedResultsQuery("SELECT t.id, u.id FROM t LEFT OUTER JOIN u ON t.id=u.t_id"); + expect(connection.queries.length, 1); + match = regex.firstMatch(connection.queries.first); + expect(oids.contains(match.group(1)), false); + oids.add(match.group(1)); + connection.queries.clear(); + + await connection.mappedResultsQuery("SELECT u.id FROM u"); + expect(connection.queries.length, 0); + }); + + test("Non-table mappedResultsQuery succeeds", () async { + final result = await connection.mappedResultsQuery("SELECT 1"); + expect(result, [{null: {"?column?": 1}}]); + }); +} + +class InterceptingConnection extends PostgreSQLConnection { + InterceptingConnection(String host, int port, String databaseName, {String username: null, String password: null}) + : super(host, port, databaseName, username: username, password: password); + + List queries = []; + + @override + Future>> query(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}) { + queries.add(fmtString); + return super.query(fmtString, substitutionValues: substitutionValues, allowReuse: allowReuse); + } +} diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 3eeb366..808fc42 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -379,6 +379,25 @@ void main() { }); expect(result, []); }); + + test("If exception thrown while preparing query, transaction gets rolled back", () async { + try { + await conn.transaction((c) async { + await c.query("INSERT INTO t (id) VALUES (1)"); + + c.query("INSERT INTO t (id) VALUES (@id:int4)", substitutionValues: { + "id": "foobar" + }); + await c.query("INSERT INTO t (id) VALUES (2)"); + }); + expect(true, false); + } catch (e) { + expect(e is FormatException, true); + } + + var noRows = await conn.query("SELECT id FROM t"); + expect(noRows, []); + }); }); group("Transaction:Rollback recovery", () { From 0f4e3d6c96b06ecaf90f38f1c5f3a4816190a694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 24 Mar 2018 03:59:21 +0100 Subject: [PATCH 18/73] Preserve error stacktrace on various query or transaction errors. (#30) --- CHANGELOG.md | 4 ++++ lib/src/connection.dart | 2 +- lib/src/connection_fsm.dart | 8 ++++---- lib/src/query.dart | 4 ++-- lib/src/transaction_proxy.dart | 8 ++++---- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acff220..4d3e116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.8 + +- Preserve error stacktrace on various query or transaction errors. + ## 0.9.7 - Adds `Connection.mappedResultsQuery` to return query results as a `Map` with keys for table and column names. diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 1f44ab4..2b8040c 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -420,7 +420,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { var exception = new PostgreSQLException("Connection closed or query cancelled (reason: $error).", stackTrace: stackTrace); queries?.forEach((q) { - q.completeError(exception); + q.completeError(exception, stackTrace); }); }); } diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index ff9426d..f5c41be 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -182,9 +182,9 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { q.sendExtended(connection._socket, cacheQuery: cached); return new _PostgreSQLConnectionStateBusy(q); - } catch (e) { + } catch (e, st) { scheduleMicrotask(() { - q.completeError(e); + q.completeError(e, st); connection._transitionToState(new _PostgreSQLConnectionStateIdle()); }); @@ -301,9 +301,9 @@ class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnection q.sendExtended(connection._socket, cacheQuery: cached); return new _PostgreSQLConnectionStateBusy(q); - } catch (e) { + } catch (e, st) { scheduleMicrotask(() { - q.completeError(e); + q.completeError(e, st); connection._transitionToState(new _PostgreSQLConnectionStateTransactionFailure(transaction)); }); diff --git a/lib/src/query.dart b/lib/src/query.dart index c9ecbdf..e77dd5c 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -138,8 +138,8 @@ class Query { _onComplete.complete(rows); } - void completeError(dynamic error) { - _onComplete.completeError(error); + void completeError(dynamic error, [StackTrace stackTrace]) { + _onComplete.completeError(error, stackTrace); } String toString() => statement; diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index b342e2f..dc60566 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -78,11 +78,11 @@ class _TransactionProxy implements PostgreSQLExecutionContext { await execute("ROLLBACK"); completer.complete(new PostgreSQLRollback._(rollback.reason)); return; - } catch (e) { + } catch (e, st) { queryQueue = []; await execute("ROLLBACK"); - completer.completeError(e); + completer.completeError(e, st); return; } @@ -104,11 +104,11 @@ class _TransactionProxy implements PostgreSQLExecutionContext { connection._cacheQuery(query); queryQueue.remove(query); - } catch (e) { + } catch (e, st) { queryQueue = []; await execute("ROLLBACK"); - completer.completeError(e); + completer.completeError(e, st); return null; } From 5efc3f59ef2706913c6b057c3b49b5f1ed18a97f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 24 Mar 2018 16:57:31 +0100 Subject: [PATCH 19/73] Minimal set of linter rules in analysis_options.yaml (#32) --- .analysis_options | 13 ------------- analysis_options.yaml | 20 ++++++++++++++++++++ test/transaction_test.dart | 2 ++ 3 files changed, 22 insertions(+), 13 deletions(-) delete mode 100644 .analysis_options create mode 100644 analysis_options.yaml diff --git a/.analysis_options b/.analysis_options deleted file mode 100644 index cac765f..0000000 --- a/.analysis_options +++ /dev/null @@ -1,13 +0,0 @@ -# This file allows you to configure the Dart analyzer. -# -# The commented part below is just for inspiration. Read the guide here: -# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer - - analyzer: - strong-mode: true -# excludes: -# - path/to/excluded/files/** -# linter: -# rules: -# # see catalogue here: http://dart-lang.github.io/linter/lints/ -# - hash_and_equals \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..4205311 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,20 @@ +# This file allows you to configure the Dart analyzer. +# +# The commented part below is just for inspiration. Read the guide here: +# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer + +analyzer: + strong-mode: true +# excludes: +# - path/to/excluded/files/** + +linter: + rules: + # see catalogue here: http://dart-lang.github.io/linter/lints/ + - camel_case_types + - hash_and_equals + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - unawaited_futures + - unrelated_type_equality_checks + - valid_regexps diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 808fc42..fad4e9e 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unawaited_futures + import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; import 'dart:async'; From a15b854c73d795963c939d954099b4a000937c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 26 Mar 2018 16:12:39 +0200 Subject: [PATCH 20/73] Read support for BYTEA type. (#35) --- CHANGELOG.md | 1 + lib/src/postgresql_codec.dart | 4 ++++ test/decode_test.dart | 17 ++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3e116..e6c84ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.9.8 - Preserve error stacktrace on various query or transaction errors. +- Read support for `BYEA` columns. ## 0.9.7 diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart index 22ea1a4..ba2956a 100644 --- a/lib/src/postgresql_codec.dart +++ b/lib/src/postgresql_codec.dart @@ -49,6 +49,7 @@ enum PostgreSQLDataType { /// A namespace for data encoding and decoding operations for PostgreSQL data. abstract class PostgreSQLCodec { static const int TypeBool = 16; + static const int TypeByteArray = 17; static const int TypeInt8 = 20; static const int TypeInt2 = 21; static const int TypeInt4 = 23; @@ -424,6 +425,9 @@ abstract class PostgreSQLCodec { return JSON.decode(string); } + case TypeByteArray: + return value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes); + default: return UTF8.decode( value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); diff --git a/test/decode_test.dart b/test/decode_test.dart index 6ce63ce..9a50741 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -10,23 +10,23 @@ void main() { await connection.execute(""" CREATE TEMPORARY TABLE t ( i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, - t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb) + t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb, ba bytea) """); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) " + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba) " "VALUES (-2147483648, -9223372036854775808, TRUE, -32768, " "'string', 10.0, 10.0, '1983-11-06', " "'1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000', " - "'{\"key\":\"value\"}')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) " + "'{\"key\":\"value\"}', E'\\\\000')"); + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba) " "VALUES (2147483647, 9223372036854775807, FALSE, 32767, " "'a significantly longer string to the point where i doubt this actually matters', " "10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', " "'2183-11-06 00:00:00.999999', " - "'[{\"key\":1}]')"); + "'[{\"key\":1}]', E'\\\\377')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) " - "VALUES (null, null, null, null, null, null, null, null, null, null, null)"); + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba) " + "VALUES (null, null, null, null, null, null, null, null, null, null, null, null)"); }); tearDown(() async { await connection?.close(); @@ -55,6 +55,7 @@ void main() { expect(row1[10], equals(new DateTime.utc(1983, 11, 6, 6))); expect(row1[11], equals(new DateTime.utc(1983, 11, 6, 6))); expect(row1[12], equals({"key": "value"})); + expect(row1[13], equals([0])); // upper bound row expect(row2[0], equals(2147483647)); @@ -76,6 +77,7 @@ void main() { equals([ {"key": 1} ])); + expect(row2[13], equals([255])); // all null row expect(row3[0], isNull); @@ -91,6 +93,7 @@ void main() { expect(row3[10], isNull); expect(row3[11], isNull); expect(row3[12], isNull); + expect(row3[13], isNull); }); test("Fetch/insert empty string", () async { From f21df221fa4874a56ab878d5ba5bcd4d260e02e0 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 27 Mar 2018 21:29:55 -0400 Subject: [PATCH 21/73] Fixes issue where transaction query rows are Iterable, and not List (#34) * toList() row iterables in transaction * Adds an add'l few expects for test * Move toList up to source, preventit from needing to be called in two places --- lib/src/connection.dart | 14 +++++------ lib/src/query.dart | 2 +- test/transaction_test.dart | 48 +++++++++++++++++++++----------------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 2b8040c..cdc21ff 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -223,8 +223,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { query.statementIdentifier = _reuseIdentifierForQuery(query); } - final rows = await _enqueue(query); - return rows.map((Iterable row) => row.toList()).toList(); + return _enqueue(query); } /// Executes a query on this connection and returns each row as a [Map]. @@ -285,8 +284,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } - var query = new Query(fmtString, substitutionValues, this, null) - ..onlyReturnAffectedRowCount = true; + var query = new Query(fmtString, substitutionValues, this, null)..onlyReturnAffectedRowCount = true; return _enqueue(query); } @@ -342,8 +340,8 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName final tableOIDs = new Set.from(columns.map((f) => f.tableID)); - final unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList() - ..sort((int lhs, int rhs) => lhs.compareTo(rhs)); + final List unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); + unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); if (unresolvedTableOIDs.isNotEmpty) { await _resolveTableOIDs(unresolvedTableOIDs); @@ -369,8 +367,8 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { Future _resolveTableOIDs(List oids) async { final unresolvedIDString = oids.join(","); - final orderedTableNames = await query( - "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + final orderedTableNames = + await query("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); final iterator = oids.iterator; orderedTableNames.forEach((tableName) { diff --git a/lib/src/query.dart b/lib/src/query.dart index e77dd5c..210224a 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -126,7 +126,7 @@ class Query { return PostgreSQLCodec.decodeValue(bd, iterator.current.typeID); }); - rows.add(lazyDecodedData); + rows.add(lazyDecodedData.toList()); } void complete(int rowsAffected) { diff --git a/test/transaction_test.dart b/test/transaction_test.dart index fad4e9e..cf9df3c 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -9,8 +9,7 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -19,8 +18,23 @@ void main() { await conn?.close(); }); - test("Send successful transaction succeeds, returns returned value", - () async { + test("Rows are Lists of column values", () async { + await conn.execute("INSERT INTO t (id) VALUES (1)"); + + final List> outValue = await conn.transaction((ctx) async { + return await ctx.query( + 'SELECT * FROM t WHERE id = @id LIMIT 1', + substitutionValues: {'id': 1}); + }); + + expect(outValue.length, 1); + expect(outValue.first is List, true); + expect(outValue.first.length, 1); + expect(outValue.first.first, 1); + }); + + + test("Send successful transaction succeeds, returns returned value", () async { var outResult = await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); @@ -36,8 +50,7 @@ void main() { ]); }); - test("Query during transaction must wait until transaction is finished", - () async { + test("Query during transaction must wait until transaction is finished", () async { var orderEnsurer = []; var nextCompleter = new Completer.sync(); var outResult = conn.transaction((c) async { @@ -70,8 +83,7 @@ void main() { ]); }); - test("Make sure two simultaneous transactions cannot be interwoven", - () async { + test("Make sure two simultaneous transactions cannot be interwoven", () async { var orderEnsurer = []; var firstTransactionFuture = conn.transaction((c) async { @@ -126,8 +138,7 @@ void main() { expect(result, []); }); - test("Intentional rollback from outside of a transaction has no impact", - () async { + test("Intentional rollback from outside of a transaction has no impact", () async { var orderEnsurer = []; var nextCompleter = new Completer.sync(); var outResult = conn.transaction((c) async { @@ -185,9 +196,7 @@ void main() { ]); }); - test( - "A transaction with a rollback and non-await queries rolls back transaction", - () async { + test("A transaction with a rollback and non-await queries rolls back transaction", () async { conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)"); ctx.query("INSERT INTO t (id) VALUES (2)"); @@ -207,8 +216,7 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -306,8 +314,7 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -387,9 +394,7 @@ void main() { await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); - c.query("INSERT INTO t (id) VALUES (@id:int4)", substitutionValues: { - "id": "foobar" - }); + c.query("INSERT INTO t (id) VALUES (@id:int4)", substitutionValues: {"id": "foobar"}); await c.query("INSERT INTO t (id) VALUES (2)"); }); expect(true, false); @@ -406,8 +411,7 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); From cd8da6264c54b4bff3a6d69dcb11740ab789338a Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 27 Mar 2018 21:47:51 -0400 Subject: [PATCH 22/73] Bump version number to 0.9.8 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ddb08af..b19378c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 0.9.7 +version: 0.9.8 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: From 9d82d120196dcd85d7f8ec45f8e35734fedc5e75 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 27 Mar 2018 21:48:46 -0400 Subject: [PATCH 23/73] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2205f7b..147aa2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Files and directories created by pub .packages +.dart_tool/ .pub/ build/ packages From 8f542f1411160a76f859b518e583efbe7857f445 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Sun, 1 Apr 2018 20:09:58 -0400 Subject: [PATCH 24/73] Adds tests and fixes some async issues in transaction (#37) * Adds tests and fixes some async issues in transaction * remove extra catch --- lib/src/transaction_proxy.dart | 25 ++++++++++-------- test/transaction_test.dart | 47 +++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index dc60566..e308cf7 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -29,6 +29,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { List> queryQueue = []; PostgreSQLConnection connection; _TransactionQuerySignature executionBlock; + bool _hasFailed = false; Future commit() async { await execute("COMMIT"); @@ -79,10 +80,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { completer.complete(new PostgreSQLRollback._(rollback.reason)); return; } catch (e, st) { - queryQueue = []; - - await execute("ROLLBACK"); - completer.completeError(e, st); + await _transactionFailed(e, st); return; } @@ -98,21 +96,26 @@ class _TransactionProxy implements PostgreSQLExecutionContext { queryQueue.add(query); connection._transitionToState(connection._connectionState.awake()); - var result = null; try { - result = await query.future; + final result = await query.future; connection._cacheQuery(query); queryQueue.remove(query); + + return result; } catch (e, st) { - queryQueue = []; + await _transactionFailed(e, st); + rethrow; + } + } + Future _transactionFailed(dynamic error, [StackTrace trace]) async { + if (!_hasFailed) { + _hasFailed = true; + queryQueue = []; await execute("ROLLBACK"); - completer.completeError(e, st); - return null; + completer.completeError(error, trace); } - - return result; } } diff --git a/test/transaction_test.dart b/test/transaction_test.dart index cf9df3c..a8afe52 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -22,9 +22,7 @@ void main() { await conn.execute("INSERT INTO t (id) VALUES (1)"); final List> outValue = await conn.transaction((ctx) async { - return await ctx.query( - 'SELECT * FROM t WHERE id = @id LIMIT 1', - substitutionValues: {'id': 1}); + return await ctx.query('SELECT * FROM t WHERE id = @id LIMIT 1', substitutionValues: {'id': 1}); }); expect(outValue.length, 1); @@ -33,7 +31,6 @@ void main() { expect(outValue.first.first, 1); }); - test("Send successful transaction succeeds, returns returned value", () async { var outResult = await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); @@ -121,13 +118,16 @@ void main() { }); test("May intentionally rollback transaction", () async { + var reached = false; await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); c.cancelTransaction(); + reached = true; await c.query("INSERT INTO t (id) VALUES (2)"); }); + expect(reached, false); var result = await conn.query("SELECT id FROM t"); expect(result, []); }); @@ -197,7 +197,7 @@ void main() { }); test("A transaction with a rollback and non-await queries rolls back transaction", () async { - conn.transaction((ctx) async { + await conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)"); ctx.query("INSERT INTO t (id) VALUES (2)"); ctx.cancelTransaction(); @@ -394,7 +394,7 @@ void main() { await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); - c.query("INSERT INTO t (id) VALUES (@id:int4)", substitutionValues: {"id": "foobar"}); + c.query("INSERT INTO t (id) VALUES (@id:int4)", substitutionValues: {"id": "foobar"}).catchError((_) => null); await c.query("INSERT INTO t (id) VALUES (2)"); }); expect(true, false); @@ -405,6 +405,41 @@ void main() { var noRows = await conn.query("SELECT id FROM t"); expect(noRows, []); }); + + test("Async query failure prevents closure from continuning", () async { + var reached = false; + + try { + await conn.transaction((c) async { + await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query("INSERT INTO t (id) VALUE ('foo') RETURNING id"); + + reached = true; + await c.query("INSERT INTO t (id) VALUES (2)"); + }); + fail('unreachable'); + } on PostgreSQLException { + } + + expect(reached, false); + final res = await conn.query("SELECT * FROM t"); + expect(res, []); + }); + + test("When exception thrown in unawaited on future, transaction is rolled back", () async { + try { + await conn.transaction((c) async { + await c.query("INSERT INTO t (id) VALUES (1)"); + c.query("INSERT INTO t (id) VALUE ('foo') RETURNING id").catchError((_) => null); + await c.query("INSERT INTO t (id) VALUES (2)"); + }); + fail('unreachable'); + } on PostgreSQLException { + } + + final res = await conn.query("SELECT * FROM t"); + expect(res, []); + }); }); group("Transaction:Rollback recovery", () { From 1d145caf685150a6cd72adbb67b80f1f8777c871 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 3 Apr 2018 15:38:00 -0400 Subject: [PATCH 25/73] =?UTF-8?q?refactor=20query=20caching=20into=20a=20s?= =?UTF-8?q?eparate=20object=20to=20reduce=20code=20in=20Query=E2=80=A6=20(?= =?UTF-8?q?#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor query caching into a separate object to reduce code in Query object * code style --- lib/src/connection.dart | 44 +++++----------------------------- lib/src/connection_fsm.dart | 4 ++-- lib/src/query.dart | 12 +++++----- lib/src/query_cache.dart | 37 ++++++++++++++++++++++++++++ lib/src/transaction_proxy.dart | 4 ++-- test/query_reuse_test.dart | 11 +++++---- 6 files changed, 59 insertions(+), 53 deletions(-) create mode 100644 lib/src/query_cache.dart diff --git a/lib/src/connection.dart b/lib/src/connection.dart index cdc21ff..971338a 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -3,6 +3,8 @@ library postgres.connection; import 'dart:async'; import 'dart:typed_data'; +import 'package:postgres/src/query_cache.dart'; + import 'message_window.dart'; import 'query.dart'; @@ -126,12 +128,10 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { /// Prior to connection, it is the empty map. Map settings = {}; + QueryCache _cache = new QueryCache(); Socket _socket; MessageFramer _framer = new MessageFramer(); - Map _reuseMap = {}; - int _reuseCounter = 0; - Map _tableOIDNameMap = {}; int _secretKey; @@ -220,7 +220,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { var query = new Query>>(fmtString, substitutionValues, this, null); if (allowReuse) { - query.statementIdentifier = _reuseIdentifierForQuery(query); + query.statementIdentifier = _cache.identifierForQuery(query); } return _enqueue(query); @@ -264,7 +264,7 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { var query = new Query>>(fmtString, substitutionValues, this, null); if (allowReuse) { - query.statementIdentifier = _reuseIdentifierForQuery(query); + query.statementIdentifier = _cache.identifierForQuery(query); } final rows = await _enqueue(query); @@ -395,10 +395,9 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { var result = null; try { result = await query.future; - _cacheQuery(query); + _cache.add(query); _queryQueue.remove(query); } catch (e) { - _cacheQuery(query); _queryQueue.remove(query); rethrow; } @@ -509,37 +508,6 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { throw new PostgreSQLException("SSL not allowed for this connection."); } - void _cacheQuery(Query query) { - if (query.cache == null) { - return; - } - - if (query.cache.isValid) { - _reuseMap[query.statement] = query.cache; - } - } - - QueryCache _cachedQuery(String statementIdentifier) { - if (statementIdentifier == null) { - return null; - } - - return _reuseMap[statementIdentifier]; - } - - String _reuseIdentifierForQuery(Query q) { - var existing = _reuseMap[q.statement]; - if (existing != null) { - return existing.preparedStatementName; - } - - var string = "$_reuseCounter".padLeft(12, "0"); - - _reuseCounter++; - - return string; - } - Future _cleanup() async { await _notifications.close(); } diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index f5c41be..536bc16 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -178,7 +178,7 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { return new _PostgreSQLConnectionStateBusy(q); } - var cached = connection._cachedQuery(q.statement); + final cached = connection._cache[q.statement]; q.sendExtended(connection._socket, cacheQuery: cached); return new _PostgreSQLConnectionStateBusy(q); @@ -297,7 +297,7 @@ class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnection return new _PostgreSQLConnectionStateBusy(q); } - var cached = connection._cachedQuery(q.statement); + final cached = connection._cache[q.statement]; q.sendExtended(connection._socket, cacheQuery: cached); return new _PostgreSQLConnectionStateBusy(q); diff --git a/lib/src/query.dart b/lib/src/query.dart index 210224a..44da7be 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -35,7 +35,7 @@ class Query { List rows = []; - QueryCache cache; + CachedQuery cache; void sendSimple(Socket socket) { var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues); @@ -44,7 +44,7 @@ class Query { socket.add(queryMessage.asBytes()); } - void sendExtended(Socket socket, {QueryCache cacheQuery: null}) { + void sendExtended(Socket socket, {CachedQuery cacheQuery: null}) { if (cacheQuery != null) { fieldDescriptions = cacheQuery.fieldDescriptions; sendCachedQuery(socket, cacheQuery, substitutionValues); @@ -74,13 +74,13 @@ class Query { ]; if (statementIdentifier != null) { - cache = new QueryCache(statementIdentifier, formatIdentifiers); + cache = new CachedQuery(statementIdentifier, formatIdentifiers); } socket.add(ClientMessage.aggregateBytes(messages)); } - void sendCachedQuery(Socket socket, QueryCache cacheQuery, Map substitutionValues) { + void sendCachedQuery(Socket socket, CachedQuery cacheQuery, Map substitutionValues) { var statementName = cacheQuery.preparedStatementName; var parameterList = cacheQuery.orderedParameters.map((identifier) => encodeParameter(identifier, substitutionValues)).toList(); @@ -145,8 +145,8 @@ class Query { String toString() => statement; } -class QueryCache { - QueryCache(this.preparedStatementName, this.orderedParameters); +class CachedQuery { + CachedQuery(this.preparedStatementName, this.orderedParameters); String preparedStatementName; List orderedParameters; diff --git a/lib/src/query_cache.dart b/lib/src/query_cache.dart new file mode 100644 index 0000000..7c83680 --- /dev/null +++ b/lib/src/query_cache.dart @@ -0,0 +1,37 @@ +import 'package:postgres/src/query.dart'; + +class QueryCache { + final Map queries = {}; + int idCounter = 0; + + void add(Query query) { + if (query.cache == null) { + return; + } + + if (query.cache.isValid) { + queries[query.statement] = query.cache; + } + } + + operator [](String statementId) { + if (statementId == null) { + return null; + } + + return queries[statementId]; + } + + String identifierForQuery(Query query) { + var existing = queries[query.statement]; + if (existing != null) { + return existing.preparedStatementName; + } + + var string = "$idCounter".padLeft(12, "0"); + + idCounter++; + + return string; + } +} \ No newline at end of file diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index e308cf7..692aba8 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -47,7 +47,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { fmtString, substitutionValues, connection, this); if (allowReuse) { - query.statementIdentifier = connection._reuseIdentifierForQuery(query); + query.statementIdentifier = connection._cache.identifierForQuery(query); } return enqueue(query); @@ -99,7 +99,7 @@ class _TransactionProxy implements PostgreSQLExecutionContext { try { final result = await query.future; - connection._cacheQuery(query); + connection._cache.add(query); queryQueue.remove(query); return result; diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index 8dc928b..22decb9 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -481,7 +481,7 @@ void main() { }); test( - "A failed bind on initial query fails query, but cached query is available", + "A failed bind on initial query fails query, but can still make query later", () async { var string = "insert into u (i1, i2) values (@i1, @i2) returning i1, i2"; try { @@ -491,7 +491,7 @@ void main() { expect(true, false); } on PostgreSQLException {} - expect(hasCachedQueryNamed(connection, string), true); + expect(hasCachedQueryNamed(connection, string), false); var results = await connection.query("select i1, i2 from u"); expect(results, []); @@ -501,6 +501,7 @@ void main() { expect(results, [ [1, 2] ]); + expect(hasCachedQueryNamed(connection, string), true); }); test( @@ -557,9 +558,9 @@ void main() { } Map cachedQueryMap(PostgreSQLConnection connection) { - var reuseMapMirror = reflect(connection).type.declarations.values.firstWhere( - (DeclarationMirror dm) => dm.simpleName.toString().contains("_reuseMap")); - return reflect(connection).getField(reuseMapMirror.simpleName).reflectee + var cacheMirror = reflect(connection).type.declarations.values.firstWhere( + (DeclarationMirror dm) => dm.simpleName.toString().contains("_cache")); + return reflect(connection).getField(cacheMirror.simpleName).getField(#queries).reflectee as Map; } From 44b36860bd4a2c0e2abc6fe23a761d4dcf31aa90 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 3 Apr 2018 19:30:45 -0400 Subject: [PATCH 26/73] Refactors connections and transactions to unify their behavior (#40) * refactor query caching into a separate object to reduce code in Query object * wip * code style * Ensure transactions with unawaited futures are properly handled on error --- lib/src/connection.dart | 357 ++++++++++++--------------------- lib/src/connection_fsm.dart | 6 +- lib/src/execution_context.dart | 74 +++++++ lib/src/query.dart | 2 + lib/src/query_queue.dart | 57 ++++++ lib/src/transaction_proxy.dart | 98 +++------ test/connection_test.dart | 209 ++++++++----------- test/query_reuse_test.dart | 1 + test/transaction_test.dart | 19 +- 9 files changed, 386 insertions(+), 437 deletions(-) create mode 100644 lib/src/execution_context.dart create mode 100644 lib/src/query_queue.dart diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 971338a..ed8fe01 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:postgres/src/query_cache.dart'; +import 'package:postgres/src/execution_context.dart'; +import 'package:postgres/src/query_queue.dart'; import 'message_window.dart'; import 'query.dart'; @@ -18,51 +20,11 @@ part 'transaction_proxy.dart'; part 'exceptions.dart'; -abstract class PostgreSQLExecutionContext { - /// Executes a query on this context. - /// - /// This method sends the query described by [fmtString] to the database and returns a [Future] whose value is the returned rows from the query after the query completes. - /// The format string may contain parameters that are provided in [substitutionValues]. Parameters are prefixed with the '@' character. Keys to replace the parameters - /// do not include the '@' character. For example: - /// - /// connection.query("SELECT * FROM table WHERE id = @idParam", {"idParam" : 2}); - /// - /// The type of the value is inferred by default, but can be made more specific by adding ':type" to the parameter pattern in the format string. The possible values - /// are declared as static variables in [PostgreSQLCodec] (e.g., [PostgreSQLCodec.TypeInt4]). For example: - /// - /// connection.query("SELECT * FROM table WHERE id = @idParam:int4", {"idParam" : 2}); - /// - /// You may also use [PostgreSQLFormat.id] to create parameter patterns. - /// - /// If successful, the returned [Future] completes with a [List] of rows. Each is row is represented by a [List] of column values for that row that were returned by the query. - /// - /// By default, instances of this class will reuse queries. This allows significantly more efficient transport to and from the database. You do not have to do - /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is - /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. - Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}); - - /// Executes a query on this context. - /// - /// This method sends a SQL string to the database this instance is connected to. Parameters can be provided in [fmtString], see [query] for more details. - /// - /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command - /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, - /// or return rows. - Future execute(String fmtString, {Map substitutionValues: null}); - - /// Cancels a transaction on this context. - /// - /// If this context is an instance of [PostgreSQLConnection], this method has no effect. If the context is a transaction context (passed as the argument - /// to [PostgreSQLConnection.transaction]), this will rollback the transaction. - void cancelTransaction({String reason: null}); -} - /// Instances of this class connect to and communicate with a PostgreSQL database. /// /// The primary type of this library, a connection is responsible for connecting to databases and executing queries. /// A connection may be opened with [open] after it is created. -class PostgreSQLConnection implements PostgreSQLExecutionContext { +class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { /// Creates an instance of [PostgreSQLConnection]. /// /// [host] must be a hostname, e.g. "foobar.com" or IP address. Do not include scheme or port. @@ -132,22 +94,14 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { Socket _socket; MessageFramer _framer = new MessageFramer(); - Map _tableOIDNameMap = {}; - int _secretKey; List _salt; bool _hasConnectedPreviously = false; _PostgreSQLConnectionState _connectionState; - List> _queryQueue = []; - - Query get _pendingQuery { - if (_queryQueue.isEmpty) { - return null; - } - return _queryQueue.first; - } + PostgreSQLExecutionContext get _transaction => null; + PostgreSQLConnection get _connection => this; /// Establishes a connection with a PostgreSQL database. /// @@ -186,109 +140,11 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { await _socket?.close(); - _cancelCurrentQueries(); + _queue.cancel(); return _cleanup(); } - /// Executes a query on this connection. - /// - /// This method sends the query described by [fmtString] to the database and returns a [Future] whose value returned rows from the query after the query completes. - /// The format string may contain parameters that are provided in [substitutionValues]. Parameters are prefixed with the '@' character. Keys to replace the parameters - /// do not include the '@' character. For example: - /// - /// connection.query("SELECT * FROM table WHERE id = @idParam", {"idParam" : 2}); - /// - /// The type of the value is inferred by default, but can be made more specific by adding ':type" to the parameter pattern in the format string. The possible values - /// are declared as static variables in [PostgreSQLCodec] (e.g., [PostgreSQLCodec.TypeInt4]). For example: - /// - /// connection.query("SELECT * FROM table WHERE id = @idParam:int4", {"idParam" : 2}); - /// - /// You may also use [PostgreSQLFormat.id] to create parameter patterns. - /// - /// If successful, the returned [Future] completes with a [List] of rows. Each is row is represented by a [List] of column values for that row that were returned by the query. - /// - /// By default, instances of this class will reuse queries. This allows significantly more efficient transport to and from the database. You do not have to do - /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is - /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. - /// - Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}) async { - if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); - } - - var query = new Query>>(fmtString, substitutionValues, this, null); - if (allowReuse) { - query.statementIdentifier = _cache.identifierForQuery(query); - } - - return _enqueue(query); - } - - /// Executes a query on this connection and returns each row as a [Map]. - /// - /// This method constructs and executes a query in the same way as [query], but returns each row as a [Map]. - /// - /// (Note: this method will execute additional queries to resolve table names the first time a table is encountered. These table names are cached per instance of this type.) - /// - /// Each row map contains key-value pairs for every table in the query. The value is a [Map] that contains - /// key-value pairs for each column from that table. For example, consider - /// the following query: - /// - /// SELECT employee.id, employee.name FROM employee; - /// - /// This method would return the following structure: - /// - /// [ - /// {"employee" : {"name": "Bob", "id": 1}} - /// ] - /// - /// The purpose of this nested structure is to disambiguate columns that have the same name in different tables. For example, consider a query with a SQL JOIN: - /// - /// SELECT employee.id, employee.name, company.name FROM employee LEFT OUTER JOIN company ON employee.company_id=company.id; - /// - /// Each returned [Map] would contain `employee` and `company` keys. The `name` key would be present in both inner maps. - /// - /// [ - /// { - /// "employee": {"name": "Bob", "id": 1}, - /// "company: {"name": "stable|kernel"} - /// } - /// ] - Future>>> mappedResultsQuery(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}) async { - if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); - } - - var query = new Query>>(fmtString, substitutionValues, this, null); - if (allowReuse) { - query.statementIdentifier = _cache.identifierForQuery(query); - } - - final rows = await _enqueue(query); - - return _mapifyRows(rows, query.fieldDescriptions); - } - - /// Executes a query on this connection. - /// - /// This method sends a SQL string to the database this instance is connected to. Parameters can be provided in [fmtString], see [query] for more details. - /// - /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command - /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, - /// or return rows. - Future execute(String fmtString, {Map substitutionValues: null}) { - if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); - } - - var query = new Query(fmtString, substitutionValues, this, null)..onlyReturnAffectedRowCount = true; - - return _enqueue(query); - } - /// Executes a series of queries inside a transaction on this connection. /// /// Queries executed inside [queryBlock] will be grouped together in a transaction. The return value of the [queryBlock] @@ -329,99 +185,20 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { } void cancelTransaction({String reason: null}) { - // We aren't in a transaction if sent to PostgreSQLConnection instances, so this is a no-op. + // Default is no-op } //////// - Future>>> _mapifyRows( - List> rows, List columns) async { - //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. - // It's not a significant impact here, but an area for optimization. This includes - // assigning resolvedTableName - final tableOIDs = new Set.from(columns.map((f) => f.tableID)); - final List unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); - unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); - - if (unresolvedTableOIDs.isNotEmpty) { - await _resolveTableOIDs(unresolvedTableOIDs); - } - - columns.forEach((desc) { - desc.resolvedTableName = _tableOIDNameMap[desc.tableID]; - }); - - final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); - return rows.map((row) { - var rowMap = new Map.fromIterable(tableNames, key: (name) => name, value: (_) => {}); - - final iterator = columns.iterator; - row.forEach((column) { - iterator.moveNext(); - rowMap[iterator.current.resolvedTableName][iterator.current.fieldName] = column; - }); - - return rowMap; - }).toList(); - } - - Future _resolveTableOIDs(List oids) async { - final unresolvedIDString = oids.join(","); - final orderedTableNames = - await query("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); - - final iterator = oids.iterator; - orderedTableNames.forEach((tableName) { - iterator.moveNext(); - if (tableName.first != null) { - _tableOIDNameMap[iterator.current] = tableName.first; - } - }); - } - void _timeout() { _connectionState = new _PostgreSQLConnectionStateClosed(); _socket?.destroy(); - _cancelCurrentQueries(); + _queue.cancel(); _cleanup(); throw new PostgreSQLException("Timed out trying to connect to database postgres://$host:$port/$databaseName."); } - Future _enqueue(Query query) async { - _queryQueue.add(query); - _transitionToState(_connectionState.awake()); - - var result = null; - try { - result = await query.future; - _cache.add(query); - _queryQueue.remove(query); - } catch (e) { - _queryQueue.remove(query); - rethrow; - } - - return result; - } - - void _cancelCurrentQueries([Object error, StackTrace stackTrace]) { - error ??= "Cancelled"; - var queries = _queryQueue; - _queryQueue = []; - - // We need to jump this to the next event so that the queries - // get the error and not the close message, since completeError is - // synchronous. - scheduleMicrotask(() { - var exception = - new PostgreSQLException("Connection closed or query cancelled (reason: $error).", stackTrace: stackTrace); - queries?.forEach((q) { - q.completeError(exception, stackTrace); - }); - }); - } - void _transitionToState(_PostgreSQLConnectionState newState) { if (identical(newState, _connectionState)) { return; @@ -463,14 +240,14 @@ class PostgreSQLConnection implements PostgreSQLExecutionContext { _connectionState = new _PostgreSQLConnectionStateClosed(); _socket.destroy(); - _cancelCurrentQueries(error, stack); + _queue.cancel(error, stack); _cleanup(); } void _handleSocketClosed() { _connectionState = new _PostgreSQLConnectionStateClosed(); - _cancelCurrentQueries(); + _queue.cancel(); _cleanup(); } @@ -535,3 +312,117 @@ class Notification { /// An optional data payload accompanying this notification. final String payload; } + +abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { + Map _tableOIDNameMap = {}; + QueryQueue _queue = new QueryQueue(); + + PostgreSQLConnection get _connection; + PostgreSQLExecutionContext get _transaction; + + Future>> query(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}) async { + if (_connection.isClosed) { + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + } + + var query = new Query>>(fmtString, substitutionValues, _connection, _transaction); + if (allowReuse) { + query.statementIdentifier = _connection._cache.identifierForQuery(query); + } + + return _enqueue(query); + } + + Future>>> mappedResultsQuery(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}) async { + if (_connection.isClosed) { + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + } + + var query = new Query>>(fmtString, substitutionValues, _connection, _transaction); + if (allowReuse) { + query.statementIdentifier = _connection._cache.identifierForQuery(query); + } + + final rows = await _enqueue(query); + + return _mapifyRows(rows, query.fieldDescriptions); + } + + Future execute(String fmtString, {Map substitutionValues: null}) { + if (_connection.isClosed) { + throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + } + + var query = new Query(fmtString, substitutionValues, _connection, _transaction)..onlyReturnAffectedRowCount = true; + + return _enqueue(query); + } + + void cancelTransaction({String reason: null}); + + Future>>> _mapifyRows( + List> rows, List columns) async { + //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. + // It's not a significant impact here, but an area for optimization. This includes + // assigning resolvedTableName + final tableOIDs = new Set.from(columns.map((f) => f.tableID)); + final List unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); + unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); + + if (unresolvedTableOIDs.isNotEmpty) { + await _resolveTableOIDs(unresolvedTableOIDs); + } + + columns.forEach((desc) { + desc.resolvedTableName = _tableOIDNameMap[desc.tableID]; + }); + + final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); + return rows.map((row) { + var rowMap = new Map.fromIterable(tableNames, key: (name) => name, value: (_) => {}); + + final iterator = columns.iterator; + row.forEach((column) { + iterator.moveNext(); + rowMap[iterator.current.resolvedTableName][iterator.current.fieldName] = column; + }); + + return rowMap; + }).toList(); + } + + Future _resolveTableOIDs(List oids) async { + final unresolvedIDString = oids.join(","); + final orderedTableNames = + await query("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + + final iterator = oids.iterator; + orderedTableNames.forEach((tableName) { + iterator.moveNext(); + if (tableName.first != null) { + _tableOIDNameMap[iterator.current] = tableName.first; + } + }); + } + + Future _enqueue(Query query) async { + _queue.add(query); + _connection._transitionToState(_connection._connectionState.awake()); + + try { + final result = await query.future; + _connection._cache.add(query); + _queue.remove(query); + return result; + } catch (e, st) { + _queue.remove(query); + await _onQueryError(query, e, st); + rethrow; + } + } + + Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async { + } +} \ No newline at end of file diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 536bc16..669979d 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -163,7 +163,7 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { Completer openCompleter; _PostgreSQLConnectionState awake() { - var pendingQuery = connection._pendingQuery; + var pendingQuery = connection._queue.pending; if (pendingQuery != null) { return processQuery(pendingQuery); } @@ -232,7 +232,7 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { // We ignore NoData, as it doesn't tell us anything we don't already know // or care about. - //print("(${query.statement}) -> $message"); + // print("(${query.statement}) -> $message"); if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateTransactionError) { @@ -282,7 +282,7 @@ class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnection } _PostgreSQLConnectionState awake() { - var pendingQuery = transaction.pendingQuery; + var pendingQuery = transaction._queue.pending; if (pendingQuery != null) { return processQuery(pendingQuery); } diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart new file mode 100644 index 0000000..d842ce2 --- /dev/null +++ b/lib/src/execution_context.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +abstract class PostgreSQLExecutionContext { + /// Executes a query on this context. + /// + /// This method sends the query described by [fmtString] to the database and returns a [Future] whose value is the returned rows from the query after the query completes. + /// The format string may contain parameters that are provided in [substitutionValues]. Parameters are prefixed with the '@' character. Keys to replace the parameters + /// do not include the '@' character. For example: + /// + /// connection.query("SELECT * FROM table WHERE id = @idParam", {"idParam" : 2}); + /// + /// The type of the value is inferred by default, but can be made more specific by adding ':type" to the parameter pattern in the format string. The possible values + /// are declared as static variables in [PostgreSQLCodec] (e.g., [PostgreSQLCodec.TypeInt4]). For example: + /// + /// connection.query("SELECT * FROM table WHERE id = @idParam:int4", {"idParam" : 2}); + /// + /// You may also use [PostgreSQLFormat.id] to create parameter patterns. + /// + /// If successful, the returned [Future] completes with a [List] of rows. Each is row is represented by a [List] of column values for that row that were returned by the query. + /// + /// By default, instances of this class will reuse queries. This allows significantly more efficient transport to and from the database. You do not have to do + /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is + /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. + Future>> query(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}); + + /// Executes a query on this context. + /// + /// This method sends a SQL string to the database this instance is connected to. Parameters can be provided in [fmtString], see [query] for more details. + /// + /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command + /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, + /// or return rows. + Future execute(String fmtString, {Map substitutionValues: null}); + + /// Cancels a transaction on this context. + /// + /// If this context is an instance of [PostgreSQLConnection], this method has no effect. If the context is a transaction context (passed as the argument + /// to [PostgreSQLConnection.transaction]), this will rollback the transaction. + void cancelTransaction({String reason: null}); + + /// Executes a query on this connection and returns each row as a [Map]. + /// + /// This method constructs and executes a query in the same way as [query], but returns each row as a [Map]. + /// + /// (Note: this method will execute additional queries to resolve table names the first time a table is encountered. These table names are cached per instance of this type.) + /// + /// Each row map contains key-value pairs for every table in the query. The value is a [Map] that contains + /// key-value pairs for each column from that table. For example, consider + /// the following query: + /// + /// SELECT employee.id, employee.name FROM employee; + /// + /// This method would return the following structure: + /// + /// [ + /// {"employee" : {"name": "Bob", "id": 1}} + /// ] + /// + /// The purpose of this nested structure is to disambiguate columns that have the same name in different tables. For example, consider a query with a SQL JOIN: + /// + /// SELECT employee.id, employee.name, company.name FROM employee LEFT OUTER JOIN company ON employee.company_id=company.id; + /// + /// Each returned [Map] would contain `employee` and `company` keys. The `name` key would be present in both inner maps. + /// + /// [ + /// { + /// "employee": {"name": "Bob", "id": 1}, + /// "company: {"name": "stable|kernel"} + /// } + /// ] + Future>>> mappedResultsQuery(String fmtString, + {Map substitutionValues: null, bool allowReuse: true}); +} \ No newline at end of file diff --git a/lib/src/query.dart b/lib/src/query.dart index 44da7be..16e3fa1 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'package:postgres/src/execution_context.dart'; + import 'postgresql_codec.dart'; import 'connection.dart'; import 'dart:io'; diff --git a/lib/src/query_queue.dart b/lib/src/query_queue.dart new file mode 100644 index 0000000..4356517 --- /dev/null +++ b/lib/src/query_queue.dart @@ -0,0 +1,57 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:postgres/postgres.dart'; +import 'package:postgres/src/query.dart'; + +class QueryQueue extends ListBase> implements List> { + List> _inner = []; + + Query get pending { + if (_inner.isEmpty) { + return null; + } + return _inner.first; + } + + void cancel([Object error, StackTrace stackTrace]) { + error ??= "Cancelled"; + final existing = _inner; + _inner = []; + + // We need to jump this to the next event so that the queries + // get the error and not the close message, since completeError is + // synchronous. + scheduleMicrotask(() { + var exception = + new PostgreSQLException("Connection closed or query cancelled (reason: $error).", stackTrace: stackTrace); + existing?.forEach((q) { + q.completeError(exception, stackTrace); + }); + }); + } + + @override + set length(int newLength) { + _inner.length = newLength; + } + + @override + Query operator [](int index) => _inner[index]; + + @override + int get length => _inner.length; + + @override + void operator []=(int index, Query value) => _inner[index] = value; + + @override + void add(Query element) { + _inner.add(element); + } + + @override + void addAll(Iterable iterable) { + _inner.addAll(iterable); + } +} diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index 692aba8..c46fa8d 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -1,16 +1,12 @@ part of postgres.connection; -typedef Future _TransactionQuerySignature( - PostgreSQLExecutionContext connection); +typedef Future _TransactionQuerySignature(PostgreSQLExecutionContext connection); -class _TransactionProxy implements PostgreSQLExecutionContext { - _TransactionProxy(this.connection, this.executionBlock) { - beginQuery = new Query("BEGIN", {}, connection, this) - ..onlyReturnAffectedRowCount = true; +class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { + _TransactionProxy(this._connection, this.executionBlock) { + beginQuery = new Query("BEGIN", {}, _connection, this)..onlyReturnAffectedRowCount = true; - beginQuery.future - .then(startTransaction) - .catchError(handleTransactionQueryError); + beginQuery.future.then(startTransaction).catchError(_onBeginFailure); } Query beginQuery; @@ -18,16 +14,9 @@ class _TransactionProxy implements PostgreSQLExecutionContext { Future get future => completer.future; - Query get pendingQuery { - if (queryQueue.length > 0) { - return queryQueue.first; - } - - return null; - } + final PostgreSQLConnection _connection; + PostgreSQLExecutionContext get _transaction => this; - List> queryQueue = []; - PostgreSQLConnection connection; _TransactionQuerySignature executionBlock; bool _hasFailed = false; @@ -35,47 +24,20 @@ class _TransactionProxy implements PostgreSQLExecutionContext { await execute("COMMIT"); } - Future>> query(String fmtString, - {Map substitutionValues: null, - bool allowReuse: true}) async { - if (connection.isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); - } - - var query = new Query>>( - fmtString, substitutionValues, connection, this); - - if (allowReuse) { - query.statementIdentifier = connection._cache.identifierForQuery(query); - } - - return enqueue(query); - } - - Future execute(String fmtString, - {Map substitutionValues: null}) async { - if (connection.isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); - } - - var query = new Query(fmtString, substitutionValues, connection, this) - ..onlyReturnAffectedRowCount = true; - - return enqueue(query); - } - void cancelTransaction({String reason: null}) { throw new _TransactionRollbackException(reason); } - Future startTransaction(dynamic beginResults) async { + Future startTransaction(dynamic _) async { var result; try { result = await executionBlock(this); + + // Place another event in the queue so that any non-awaited futures + // in the executionBlock are given a chance to run + await new Future(() => null); } on _TransactionRollbackException catch (rollback) { - queryQueue = []; + _queue.clear(); await execute("ROLLBACK"); completer.complete(new PostgreSQLRollback._(rollback.reason)); return; @@ -84,39 +46,35 @@ class _TransactionProxy implements PostgreSQLExecutionContext { return; } + // If we have queries pending, we need to wait for them to complete + // before finishing !!!! + if (_queue.isNotEmpty) { + // ignore the error from this query if there is one, it'll pop up elsewhere + await _queue.last.future.catchError((_) {}); + } + await execute("COMMIT"); completer.complete(result); } - Future handleTransactionQueryError(dynamic err) async { - } - - Future enqueue(Query query) async { - queryQueue.add(query); - connection._transitionToState(connection._connectionState.awake()); - - try { - final result = await query.future; - - connection._cache.add(query); - queryQueue.remove(query); - - return result; - } catch (e, st) { - await _transactionFailed(e, st); - rethrow; - } + Future _onBeginFailure(dynamic err) async { + completer.completeError(err); } Future _transactionFailed(dynamic error, [StackTrace trace]) async { if (!_hasFailed) { _hasFailed = true; - queryQueue = []; + _queue.clear(); await execute("ROLLBACK"); completer.completeError(error, trace); } } + + @override + Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async { + await _transactionFailed(error, trace); + } } /// Represents a rollback from a transaction. diff --git a/test/connection_test.dart b/test/connection_test.dart index 9f670c3..d1442cb 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -13,8 +13,7 @@ void main() { }); test("Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await conn.open(); @@ -22,67 +21,63 @@ void main() { }); test("SSL Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart", useSSL: true); await conn.open(); expect(await conn.execute("select 1"), equals(1)); - var socketMirror = reflect(conn).type.declarations.values.firstWhere( - (DeclarationMirror dm) => - dm.simpleName.toString().contains("_socket")); - var underlyingSocket = - reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn) + .type + .declarations + .values + .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); + var underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; expect(underlyingSocket is SecureSocket, true); }); test("Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); expect(await conn.execute("select 1"), equals(1)); }); test("SSL Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); await conn.open(); expect(await conn.execute("select 1"), equals(1)); }); - test("Closing idle connection succeeds, closes underlying socket", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test("Closing idle connection succeeds, closes underlying socket", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); await conn.close(); - var socketMirror = reflect(conn).type.declarations.values.firstWhere( - (DeclarationMirror dm) => - dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = - reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn) + .type + .declarations + .values + .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); + Socket underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; expect(await underlyingSocket.done, isNotNull); conn = null; }); - test("SSL Closing idle connection succeeds, closes underlying socket", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + test("SSL Closing idle connection succeeds, closes underlying socket", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); await conn.open(); await conn.close(); - var socketMirror = reflect(conn).type.declarations.values.firstWhere( - (DeclarationMirror dm) => - dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = - reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn) + .type + .declarations + .values + .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); + Socket underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; expect(await underlyingSocket.done, isNotNull); conn = null; @@ -91,8 +86,7 @@ void main() { test( "Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); var errors = []; @@ -113,8 +107,7 @@ void main() { test( "SSL Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); await conn.open(); var errors = []; @@ -137,8 +130,7 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); }); @@ -146,9 +138,7 @@ void main() { await conn?.close(); }); - test( - "Issuing multiple queries and awaiting between each one successfully returns the right value", - () async { + test("Issuing multiple queries and awaiting between each one successfully returns the right value", () async { expect( await conn.query("select 1", allowReuse: false), equals([ @@ -176,9 +166,7 @@ void main() { ])); }); - test( - "Issuing multiple queries without awaiting are returned with appropriate values", - () async { + test("Issuing multiple queries without awaiting are returned with appropriate values", () async { var futures = [ conn.query("select 1", allowReuse: false), conn.query("select 2", allowReuse: false), @@ -217,8 +205,7 @@ void main() { }); test("Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); conn.open(); try { @@ -230,8 +217,7 @@ void main() { }); test("SSL Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); conn.open(); try { @@ -242,10 +228,8 @@ void main() { } }); - test("Starting transaction while opening connection triggers error", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test("Starting transaction while opening connection triggers error", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); conn.open(); try { @@ -258,10 +242,8 @@ void main() { } }); - test("SSL Starting transaction while opening connection triggers error", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + test("SSL Starting transaction while opening connection triggers error", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); conn.open(); try { @@ -274,10 +256,8 @@ void main() { } }); - test("Invalid password reports error, conn is closed, disables conn", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "notdart"); + test("Invalid password reports error, conn is closed, disables conn", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "notdart"); try { await conn.open(); @@ -289,10 +269,9 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("SSL Invalid password reports error, conn is closed, disables conn", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "notdart", useSSL: true); + test("SSL Invalid password reports error, conn is closed, disables conn", () async { + conn = + new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "notdart", useSSL: true); try { await conn.open(); @@ -304,10 +283,8 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("A query error maintains connectivity, allows future queries", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test("A query error maintains connectivity, allows future queries", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -322,16 +299,14 @@ void main() { await conn.execute("INSERT INTO t (i) VALUES (2)"); }); - test( - "A query error maintains connectivity, continues processing pending queries", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test("A query error maintains connectivity, continues processing pending queries", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); await conn.execute("INSERT INTO t (i) VALUES (1)"); + //ignore: unawaited_futures conn.execute("INSERT INTO t (i) VALUES (1)").catchError((err) { // ignore }); @@ -355,25 +330,26 @@ void main() { ] ]); - var queueMirror = reflect(conn).type.declarations.values.firstWhere( - (DeclarationMirror dm) => - dm.simpleName.toString().contains("_queryQueue")); - List queue = - reflect(conn).getField(queueMirror.simpleName).reflectee; + var queueMirror = reflect(conn) + .type + .instanceMembers + .values + .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_queue")); + List queue = reflect(conn).getField(queueMirror.simpleName).reflectee; expect(queue, isEmpty); }); - test( - "A query error maintains connectivity, continues processing pending transactions", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test("A query error maintains connectivity, continues processing pending transactions", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); await conn.execute("INSERT INTO t (i) VALUES (1)"); - var orderEnsurer = []; + final orderEnsurer = []; + + // this will emit a query error + //ignore: unawaited_futures conn.execute("INSERT INTO t (i) VALUES (1)").catchError((err) { orderEnsurer.add(1); // ignore @@ -392,11 +368,8 @@ void main() { expect(orderEnsurer, [2, 1, 3, 4]); }); - test( - "Building query throws error, connection continues processing pending queries", - () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test("Building query throws error, connection continues processing pending queries", () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); await conn.open(); // Make some async queries that'll exit the event loop, but then fail on a query that'll die early @@ -419,11 +392,12 @@ void main() { ] ]); - var queueMirror = reflect(conn).type.declarations.values.firstWhere( - (DeclarationMirror dm) => - dm.simpleName.toString().contains("_queryQueue")); - List queue = - reflect(conn).getField(queueMirror.simpleName).reflectee; + var queueMirror = reflect(conn) + .type + .instanceMembers + .values + .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_queue")); + List queue = reflect(conn).getField(queueMirror.simpleName).reflectee; expect(queue, isEmpty); }); }); @@ -437,9 +411,7 @@ void main() { await socket?.close(); }); - test( - "Socket fails to connect reports error, disables connection for future use", - () async { + test("Socket fails to connect reports error, disables connection for future use", () async { var conn = new PostgreSQLConnection("localhost", 5431, "dart_test"); try { @@ -450,11 +422,8 @@ void main() { await expectConnectionIsInvalid(conn); }); - test( - "SSL Socket fails to connect reports error, disables connection for future use", - () async { - var conn = new PostgreSQLConnection("localhost", 5431, "dart_test", - useSSL: true); + test("SSL Socket fails to connect reports error, disables connection for future use", () async { + var conn = new PostgreSQLConnection("localhost", 5431, "dart_test", useSSL: true); try { await conn.open(); @@ -464,19 +433,15 @@ void main() { await expectConnectionIsInvalid(conn); }); - test( - "Connection that times out throws appropriate error and cannot be reused", - () async { - serverSocket = - await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + test("Connection that times out throws appropriate error and cannot be reused", () async { + serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", - timeoutInSeconds: 2); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2); try { await conn.open(); @@ -487,19 +452,15 @@ void main() { await expectConnectionIsInvalid(conn); }); - test( - "SSL Connection that times out throws appropriate error and cannot be reused", - () async { - serverSocket = - await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + test("SSL Connection that times out throws appropriate error and cannot be reused", () async { + serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", - timeoutInSeconds: 2, useSSL: true); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2, useSSL: true); try { await conn.open(); @@ -510,11 +471,9 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("Connection that times out triggers future for pending queries", - () async { + test("Connection that times out triggers future for pending queries", () async { var openCompleter = new Completer(); - serverSocket = - await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -524,8 +483,7 @@ void main() { }); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", - timeoutInSeconds: 2); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2); conn.open().catchError((e) {}); await openCompleter.future; @@ -538,11 +496,9 @@ void main() { } }); - test("SSL Connection that times out triggers future for pending queries", - () async { + test("SSL Connection that times out triggers future for pending queries", () async { var openCompleter = new Completer(); - serverSocket = - await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -552,8 +508,7 @@ void main() { }); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", - timeoutInSeconds: 2, useSSL: true); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2, useSSL: true); conn.open().catchError((e) {}); await openCompleter.future; diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index 22decb9..8469592 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -542,6 +542,7 @@ void main() { allowReuse: false); var string = "select i1, i2 from u where i1 = @i:int4"; + // ignore: unawaited_futures connection .query(string, substitutionValues: {"i": "foo"}).catchError((e) {}); diff --git a/test/transaction_test.dart b/test/transaction_test.dart index a8afe52..8e2e2a0 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -196,6 +196,19 @@ void main() { ]); }); + test( + "A transaction doesn't have to await on queries, when the last query fails, " + "it still emits an error from the transaction", () async { + await conn.transaction((ctx) async { + ctx.query("INSERT INTO t (id) VALUES (1)"); + ctx.query("INSERT INTO t (id) VALUES (2)"); + ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((_) => null); + }); + + var total = await conn.query("SELECT id FROM t"); + expect(total, []); + }); + test("A transaction with a rollback and non-await queries rolls back transaction", () async { await conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)"); @@ -418,8 +431,7 @@ void main() { await c.query("INSERT INTO t (id) VALUES (2)"); }); fail('unreachable'); - } on PostgreSQLException { - } + } on PostgreSQLException {} expect(reached, false); final res = await conn.query("SELECT * FROM t"); @@ -434,8 +446,7 @@ void main() { await c.query("INSERT INTO t (id) VALUES (2)"); }); fail('unreachable'); - } on PostgreSQLException { - } + } on PostgreSQLException {} final res = await conn.query("SELECT * FROM t"); expect(res, []); From 1a4ddeedf85d114805f00a57a656da286e3e11dc Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Wed, 4 Apr 2018 12:52:03 -0400 Subject: [PATCH 27/73] Adds timeouts to queries (#41) * refactor query caching into a separate object to reduce code in Query object * code style * Adds configurable timeout to queries * Add some add'l test coverage --- lib/src/connection.dart | 33 ++++++++------- lib/src/execution_context.dart | 6 +-- test/connection_test.dart | 33 +++++++++++++++ test/map_return_test.dart | 2 +- test/timeout_test.dart | 76 ++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 test/timeout_test.dart diff --git a/lib/src/connection.dart b/lib/src/connection.dart index ed8fe01..73f097d 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -101,6 +101,7 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin _PostgreSQLConnectionState _connectionState; PostgreSQLExecutionContext get _transaction => null; + PostgreSQLConnection get _connection => this; /// Establishes a connection with a PostgreSQL database. @@ -318,10 +319,11 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo QueryQueue _queue = new QueryQueue(); PostgreSQLConnection get _connection; + PostgreSQLExecutionContext get _transaction; Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}) async { + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds: 30}) async { if (_connection.isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } @@ -331,11 +333,11 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo query.statementIdentifier = _connection._cache.identifierForQuery(query); } - return _enqueue(query); + return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } Future>>> mappedResultsQuery(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}) async { + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds: 30}) async { if (_connection.isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } @@ -345,30 +347,32 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo query.statementIdentifier = _connection._cache.identifierForQuery(query); } - final rows = await _enqueue(query); + final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); return _mapifyRows(rows, query.fieldDescriptions); } - Future execute(String fmtString, {Map substitutionValues: null}) { + Future execute(String fmtString, {Map substitutionValues: null, int timeoutInSeconds: 30}) { if (_connection.isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } - var query = new Query(fmtString, substitutionValues, _connection, _transaction)..onlyReturnAffectedRowCount = true; + var query = new Query(fmtString, substitutionValues, _connection, _transaction) + ..onlyReturnAffectedRowCount = true; - return _enqueue(query); + return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } void cancelTransaction({String reason: null}); Future>>> _mapifyRows( - List> rows, List columns) async { + List> rows, List columns) async { //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName final tableOIDs = new Set.from(columns.map((f) => f.tableID)); - final List unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); + final List unresolvedTableOIDs = + tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); if (unresolvedTableOIDs.isNotEmpty) { @@ -396,7 +400,7 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo Future _resolveTableOIDs(List oids) async { final unresolvedIDString = oids.join(","); final orderedTableNames = - await query("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + await query("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); final iterator = oids.iterator; orderedTableNames.forEach((tableName) { @@ -407,12 +411,12 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo }); } - Future _enqueue(Query query) async { + Future _enqueue(Query query, {int timeoutInSeconds: 30}) async { _queue.add(query); _connection._transitionToState(_connection._connectionState.awake()); try { - final result = await query.future; + final result = await query.future.timeout(new Duration(seconds: timeoutInSeconds)); _connection._cache.add(query); _queue.remove(query); return result; @@ -423,6 +427,5 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo } } - Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async { - } -} \ No newline at end of file + Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async {} +} diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index d842ce2..4f7216f 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -22,7 +22,7 @@ abstract class PostgreSQLExecutionContext { /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}); + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}); /// Executes a query on this context. /// @@ -31,7 +31,7 @@ abstract class PostgreSQLExecutionContext { /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. - Future execute(String fmtString, {Map substitutionValues: null}); + Future execute(String fmtString, {Map substitutionValues: null, int timeoutInSeconds}); /// Cancels a transaction on this context. /// @@ -70,5 +70,5 @@ abstract class PostgreSQLExecutionContext { /// } /// ] Future>>> mappedResultsQuery(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}); + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}); } \ No newline at end of file diff --git a/test/connection_test.dart b/test/connection_test.dart index d1442cb..ebce7e1 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -1,3 +1,4 @@ +// ignore_for_file: unawaited_futures import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; import 'dart:io'; @@ -526,6 +527,38 @@ void main() { } on PostgreSQLException {} }); }); + + test("If connection is closed, do not allow .execute", () async { + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + try { + await conn.execute("SELECT 1"); + fail('unreachable'); + } on PostgreSQLException catch (e) { + expect(e.toString(), contains("connection is not open")); + } + }); + + test("If connection is closed, do not allow .query", () async { + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + try { + await conn.query("SELECT 1"); + fail('unreachable'); + } on PostgreSQLException catch (e) { + expect(e.toString(), contains("connection is not open")); + } + + }); + + test("If connection is closed, do not allow .mappedResultsQuery", () async { + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + try { + await conn.mappedResultsQuery("SELECT 1"); + fail('unreachable'); + } on PostgreSQLException catch (e) { + expect(e.toString(), contains("connection is not open")); + } + + }); } Future expectConnectionIsInvalid(PostgreSQLConnection conn) async { diff --git a/test/map_return_test.dart b/test/map_return_test.dart index 68489eb..ad02f9b 100644 --- a/test/map_return_test.dart +++ b/test/map_return_test.dart @@ -133,7 +133,7 @@ class InterceptingConnection extends PostgreSQLConnection { @override Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true}) { + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}) { queries.add(fmtString); return super.query(fmtString, substitutionValues: substitutionValues, allowReuse: allowReuse); } diff --git a/test/timeout_test.dart b/test/timeout_test.dart new file mode 100644 index 0000000..2d2b40e --- /dev/null +++ b/test/timeout_test.dart @@ -0,0 +1,76 @@ +import 'package:postgres/postgres.dart'; +import 'package:test/test.dart'; +import 'dart:async'; + +void main() { + PostgreSQLConnection conn; + + setUp(() async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + await conn.open(); + await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); + }); + + tearDown(() async { + await conn?.close(); + }); + + test("Timeout fires on query while in queue does not execute query, query throws exception", () async { + //ignore: unawaited_futures + final f = conn.query("SELECT pg_sleep(2)"); + try { + await conn.query("SELECT 1", timeoutInSeconds: 1); + fail('unreachable'); + } on TimeoutException {} + + expect(f, completes); + }); + + test("Timeout fires during transaction rolls ack transaction", () async { + try { + await conn.transaction((ctx) async { + await ctx.query("INSERT INTO t (id) VALUES (1)"); + await ctx.query("SELECT pg_sleep(2)", timeoutInSeconds: 1); + }); + fail('unreachable'); + } on TimeoutException {} + + expect(await conn.query("SELECT * from t"), hasLength(0)); + }); + + test("Query on parent context for transaction completes (with error) after timeout", () async { + try { + await conn.transaction((ctx) async { + await conn.query("SELECT 1", timeoutInSeconds: 1); + await ctx.query("INSERT INTO t (id) VALUES (1)"); + }); + fail('unreachable'); + } on TimeoutException {} + + expect(await conn.query("SELECT * from t"), hasLength(0)); + }); + + test("If query is already on the wire and times out, safely throws timeoutexception and nothing else", () async { + try { + await conn.query("SELECT pg_sleep(2)", timeoutInSeconds: 1); + fail('unreachable'); + } on TimeoutException {} + }); + + test("Query times out, next query in the queue runs", () async { + //ignore: unawaited_futures + conn.query("SELECT pg_sleep(2)", timeoutInSeconds: 1).catchError((_) => null); + + expect(await conn.query("SELECT 1"), [[1]]); + }); + + test("Query that succeeds does not timeout", () async { + await conn.query("SELECT 1", timeoutInSeconds: 1); + expect(new Future.delayed(new Duration(seconds: 2)), completes); + }); + + test("Query that fails does not timeout", () async { + await conn.query("INSERT INTO t (id) VALUES ('foo')", timeoutInSeconds: 1).catchError((_) => null); + expect(new Future.delayed(new Duration(seconds: 2)), completes); + }); +} From 9b596f7a32d508793faa2298f6d97c5e3e34e412 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Thu, 5 Apr 2018 11:19:52 -0400 Subject: [PATCH 28/73] Refactors decoding and encoding postgresql types (#44) * Refactors decoding and encoding, improves test. adds bytea insert support * Adds a few more test cases for failure scenarios --- lib/postgres.dart | 2 +- lib/src/binary_codec.dart | 228 ++++++++++++++++ lib/src/connection.dart | 3 +- lib/src/postgresql_codec.dart | 436 ------------------------------ lib/src/query.dart | 96 ++++--- lib/src/server_messages.dart | 16 +- lib/src/substituter.dart | 8 +- lib/src/text_codec.dart | 160 ++++++++++++ lib/src/types.dart | 59 +++++ test/encoding_test.dart | 480 ++++++++++++++++++++++------------ test/query_test.dart | 2 +- 11 files changed, 832 insertions(+), 658 deletions(-) create mode 100644 lib/src/binary_codec.dart delete mode 100644 lib/src/postgresql_codec.dart create mode 100644 lib/src/text_codec.dart create mode 100644 lib/src/types.dart diff --git a/lib/postgres.dart b/lib/postgres.dart index ead4103..148a572 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -1,5 +1,5 @@ library postgres; export 'src/connection.dart'; -export 'src/postgresql_codec.dart'; +export 'src/types.dart'; export 'src/substituter.dart'; diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart new file mode 100644 index 0000000..abc1afe --- /dev/null +++ b/lib/src/binary_codec.dart @@ -0,0 +1,228 @@ +import 'dart:convert'; + +import 'dart:typed_data'; + +import 'package:postgres/postgres.dart'; +import 'package:postgres/src/types.dart'; + +class PostgresBinaryEncoder extends Converter { + const PostgresBinaryEncoder(this.dataType); + + final PostgreSQLDataType dataType; + + @override + Uint8List convert(dynamic value) { + if (value == null) { + return null; + } + + switch (dataType) { + case PostgreSQLDataType.boolean: { + if (value is! bool) { + throw new FormatException( + "Invalid type for parameter value. Expected: bool Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(1); + bd.setUint8(0, value ? 1 : 0); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.bigSerial: + case PostgreSQLDataType.bigInteger: { + if (value is! int) { + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(8); + bd.setInt64(0, value); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.serial: + case PostgreSQLDataType.integer: { + if (value is! int) { + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(4); + bd.setInt32(0, value); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.smallInteger: { + if (value is! int) { + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(2); + bd.setInt16(0, value); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.text: { + if (value is! String) { + throw new FormatException( + "Invalid type for parameter value. Expected: String Got: ${value + .runtimeType}"); + } + + return UTF8.encode(value); + } + case PostgreSQLDataType.real: { + if (value is! double) { + throw new FormatException( + "Invalid type for parameter value. Expected: double Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(4); + bd.setFloat32(0, value); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.double: { + if (value is! double) { + throw new FormatException( + "Invalid type for parameter value. Expected: double Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(8); + bd.setFloat64(0, value); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.date: { + if (value is! DateTime) { + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(4); + bd.setInt32(0, value.toUtc().difference(new DateTime.utc(2000)).inDays); + return bd.buffer.asUint8List(); + } + + case PostgreSQLDataType.timestampWithoutTimezone: { + if (value is! DateTime) { + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(8); + var diff = value.toUtc().difference(new DateTime.utc(2000)); + bd.setInt64(0, diff.inMicroseconds); + return bd.buffer.asUint8List(); + } + + case PostgreSQLDataType.timestampWithTimezone: { + if (value is! DateTime) { + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value + .runtimeType}"); + } + + var bd = new ByteData(8); + bd.setInt64( + 0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); + return bd.buffer.asUint8List(); + } + + case PostgreSQLDataType.json: { + var jsonBytes = UTF8.encode(JSON.encode(value)); + final outBuffer = new Uint8List(jsonBytes.length + 1); + outBuffer[0] = 1; + for (var i = 0; i < jsonBytes.length; i++) { + outBuffer[i + 1] = jsonBytes[i]; + } + + return outBuffer; + } + + case PostgreSQLDataType.byteArray: { + if (value is! List) { + throw new FormatException( + "Invalid type for parameter value. Expected: List Got: ${value + .runtimeType}"); + } + return new Uint8List.fromList(value); + } + } + + throw new PostgreSQLException("Unsupported datatype"); + } +} + +class PostgresBinaryDecoder extends Converter { + const PostgresBinaryDecoder(this.typeCode); + + final int typeCode; + + @override + dynamic convert(Uint8List value) { + final dataType = typeMap[typeCode]; + + if (value == null) { + return null; + } + + final buffer = new ByteData.view(value.buffer, value.offsetInBytes, value.lengthInBytes); + + switch (dataType) { + case PostgreSQLDataType.text: + return UTF8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); + case PostgreSQLDataType.boolean: + return buffer.getInt8(0) != 0; + case PostgreSQLDataType.smallInteger: + return buffer.getInt16(0); + case PostgreSQLDataType.serial: + case PostgreSQLDataType.integer: + return buffer.getInt32(0); + case PostgreSQLDataType.bigSerial: + case PostgreSQLDataType.bigInteger: + return buffer.getInt64(0); + case PostgreSQLDataType.real: + return buffer.getFloat32(0); + case PostgreSQLDataType.double: + return buffer.getFloat64(0); + case PostgreSQLDataType.timestampWithoutTimezone: + case PostgreSQLDataType.timestampWithTimezone: + return new DateTime.utc(2000) + .add(new Duration(microseconds: buffer.getInt64(0))); + + case PostgreSQLDataType.date: + return new DateTime.utc(2000) + .add(new Duration(days: buffer.getInt32(0))); + + case PostgreSQLDataType.json: { + // Removes version which is first character and currently always '1' + final bytes = value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1); + return JSON.decode(UTF8.decode(bytes)); + } + + case PostgreSQLDataType.byteArray: + return value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes); + } + + return value; + } + + static final Map typeMap = { + 16: PostgreSQLDataType.boolean, + 17: PostgreSQLDataType.byteArray, + 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, + 3802: PostgreSQLDataType.json + }; +} \ No newline at end of file diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 73f097d..89d3c5a 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -1,6 +1,7 @@ library postgres.connection; import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'package:postgres/src/query_cache.dart'; @@ -406,7 +407,7 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo orderedTableNames.forEach((tableName) { iterator.moveNext(); if (tableName.first != null) { - _tableOIDNameMap[iterator.current] = tableName.first; + _tableOIDNameMap[iterator.current] = UTF8.decode(tableName.first); } }); } diff --git a/lib/src/postgresql_codec.dart b/lib/src/postgresql_codec.dart deleted file mode 100644 index ba2956a..0000000 --- a/lib/src/postgresql_codec.dart +++ /dev/null @@ -1,436 +0,0 @@ -import 'dart:typed_data'; -import 'dart:convert'; -import 'connection.dart'; - -/// The set of available data types that [PostgreSQLConnection]s support. -enum PostgreSQLDataType { - /// Must be a [String]. - text, - - /// Must be an [int] (4-byte integer) - integer, - - /// Must be an [int] (2-byte integer) - smallInteger, - - /// Must be an [int] (8-byte integer) - bigInteger, - - /// Must be an [int] (autoincrementing 4-byte integer) - serial, - - /// Must be an [int] (autoincrementing 8-byte integer) - bigSerial, - - /// Must be a [double] (32-bit floating point value) - real, - - /// Must be a [double] (64-bit floating point value) - double, - - /// Must be a [bool] - boolean, - - /// Must be a [DateTime] (microsecond date and time precision) - timestampWithoutTimezone, - - /// Must be a [DateTime] (microsecond date and time precision) - timestampWithTimezone, - - /// Must be a [DateTime] (contains year, month and day only) - date, - - /// Must be encodable via [JSON.encode]. - /// - /// Values will be encoded via [JSON.encode] before being sent to the database. - json -} - -/// A namespace for data encoding and decoding operations for PostgreSQL data. -abstract class PostgreSQLCodec { - static const int TypeBool = 16; - static const int TypeByteArray = 17; - static const int TypeInt8 = 20; - static const int TypeInt2 = 21; - static const int TypeInt4 = 23; - static const int TypeText = 25; - static const int TypeFloat4 = 700; - static const int TypeFloat8 = 701; - static const int TypeDate = 1082; - static const int TypeTimestamp = 1114; - static const int TypeTimestampTZ = 1184; - static const int TypeJSONB = 3802; - - static String encode(dynamic value, - {PostgreSQLDataType dataType: null, bool escapeStrings: true}) { - if (value == null) { - return "null"; - } - - switch (dataType) { - case PostgreSQLDataType.text: - return encodeString(value.toString(), escapeStrings); - - case PostgreSQLDataType.integer: - case PostgreSQLDataType.smallInteger: - case PostgreSQLDataType.bigInteger: - case PostgreSQLDataType.serial: - case PostgreSQLDataType.bigSerial: - return encodeNumber(value); - - case PostgreSQLDataType.double: - case PostgreSQLDataType.real: - return encodeDouble(value); - - case PostgreSQLDataType.boolean: - return encodeBoolean(value); - - case PostgreSQLDataType.timestampWithoutTimezone: - case PostgreSQLDataType.timestampWithTimezone: - case PostgreSQLDataType.date: - return encodeDateTime(value); - - case PostgreSQLDataType.json: - return encodeJSON(value); - - default: - return encodeDefault(value, escapeStrings: escapeStrings); - } - } - - static Uint8List encodeBinary(dynamic value, int postgresType) { - if (value == null) { - return null; - } - - Uint8List outBuffer = null; - - if (postgresType == TypeBool) { - if (value is! bool) { - throw new FormatException( - "Invalid type for parameter value. Expected: bool Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(1); - bd.setUint8(0, value ? 1 : 0); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeInt8) { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(8); - bd.setInt64(0, value); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeInt2) { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(2); - bd.setInt16(0, value); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeInt4) { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(4); - bd.setInt32(0, value); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeText) { - if (value is! String) { - throw new FormatException( - "Invalid type for parameter value. Expected: String Got: ${value - .runtimeType}"); - } - - String val = value; - outBuffer = UTF8.encode(val); - } else if (postgresType == TypeFloat4) { - if (value is! double) { - throw new FormatException( - "Invalid type for parameter value. Expected: double Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(4); - bd.setFloat32(0, value); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeFloat8) { - if (value is! double) { - throw new FormatException( - "Invalid type for parameter value. Expected: double Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(8); - bd.setFloat64(0, value); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeDate) { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(4); - bd.setInt32(0, value.toUtc().difference(new DateTime.utc(2000)).inDays); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeTimestamp) { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(8); - var diff = value.toUtc().difference(new DateTime.utc(2000)); - bd.setInt64(0, diff.inMicroseconds); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeTimestampTZ) { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); - } - - var bd = new ByteData(8); - bd.setInt64( - 0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); - outBuffer = bd.buffer.asUint8List(); - } else if (postgresType == TypeJSONB) { - - var jsonBytes = UTF8.encode(JSON.encode(value)); - outBuffer = new Uint8List(jsonBytes.length + 1); - outBuffer[0] = 1; - for (var i = 0; i < jsonBytes.length; i++) { - outBuffer[i + 1] = jsonBytes[i]; - } - } - - return outBuffer; - } - - static String encodeString(String text, bool escapeStrings) { - if (!escapeStrings) { - return text; - } - - var backslashCodeUnit = r"\".codeUnitAt(0); - var quoteCodeUnit = r"'".codeUnitAt(0); - - var quoteCount = 0; - var backslashCount = 0; - var it = new RuneIterator(text); - while (it.moveNext()) { - if (it.current == backslashCodeUnit) { - backslashCount++; - } else if (it.current == quoteCodeUnit) { - quoteCount++; - } - } - - var buf = new StringBuffer(); - - if (backslashCount > 0) { - buf.write(" E"); - } - - buf.write("'"); - - if (quoteCount == 0 && backslashCount == 0) { - buf.write(text); - } else { - text.codeUnits.forEach((i) { - if (i == quoteCodeUnit || i == backslashCodeUnit) { - buf.writeCharCode(i); - buf.writeCharCode(i); - } else { - buf.writeCharCode(i); - } - }); - } - - buf.write("'"); - - return buf.toString(); - } - - static String encodeNumber(dynamic value) { - if (value is! num) { - throw new PostgreSQLException("Trying to encode ${value - .runtimeType}: $value as integer-like type."); - } - - if (value.isNaN) { - return "'nan'"; - } - - if (value.isInfinite) { - return value.isNegative ? "'-infinity'" : "'infinity'"; - } - - return value.toInt().toString(); - } - - static String encodeDouble(dynamic value) { - if (value is! num) { - throw new PostgreSQLException( - "Trying to encode ${value.runtimeType}: $value as double-like type."); - } - - if (value.isNaN) { - return "'nan'"; - } - - if (value.isInfinite) { - return value.isNegative ? "'-infinity'" : "'infinity'"; - } - - return value.toString(); - } - - static String encodeBoolean(dynamic value) { - if (value is! bool) { - throw new PostgreSQLException( - "Trying to encode ${value.runtimeType}: $value as boolean type."); - } - - return value ? "TRUE" : "FALSE"; - } - - static String encodeDateTime(dynamic value, {bool isDateOnly: false}) { - if (value is! DateTime) { - throw new PostgreSQLException( - "Trying to encode ${value.runtimeType}: $value as date-like type."); - } - - var string = value.toIso8601String(); - - if (isDateOnly) { - string = string.split("T").first; - } else { - if (!value.isUtc) { - var timezoneHourOffset = value.timeZoneOffset.inHours; - var timezoneMinuteOffset = value.timeZoneOffset.inMinutes % 60; - - var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, "0"); - var minuteComponent = - timezoneMinuteOffset.abs().toString().padLeft(2, "0"); - - if (timezoneHourOffset >= 0) { - hourComponent = "+${hourComponent}"; - } else { - hourComponent = "-${hourComponent}"; - } - - var timezoneString = [hourComponent, minuteComponent].join(":"); - string = [string, timezoneString].join(""); - } - } - - if (string.substring(0, 1) == "-") { - string = string.substring(1) + " BC"; - } else if (string.substring(0, 1) == "+") { - string = string.substring(1); - } - - return "'$string'"; - } - - static String encodeJSON(dynamic value) { - if (value == null) { - return "null"; - } - - if (value is String) { - return "'${JSON.encode(value)}'"; - } - - return "${JSON.encode(value)}"; - } - - static String encodeDefault(dynamic value, {bool escapeStrings: true}) { - if (value == null) { - return "null"; - } - - if (value is int) { - return encodeNumber(value); - } - - if (value is double) { - return encodeDouble(value); - } - - if (value is String) { - return encodeString(value, escapeStrings); - } - - if (value is DateTime) { - return encodeDateTime(value, isDateOnly: false); - } - - if (value is bool) { - return encodeBoolean(value); - } - - if (value is Map) { - return encodeJSON(value); - } - - throw new PostgreSQLException( - "Unknown inferred datatype from ${value.runtimeType}: $value"); - } - - static dynamic decodeValue(ByteData value, int dbTypeCode) { - if (value == null) { - return null; - } - - switch (dbTypeCode) { - case TypeBool: - return value.getInt8(0) != 0; - case TypeInt2: - return value.getInt16(0); - case TypeInt4: - return value.getInt32(0); - case TypeInt8: - return value.getInt64(0); - case TypeFloat4: - return value.getFloat32(0); - case TypeFloat8: - return value.getFloat64(0); - - case TypeTimestamp: - case TypeTimestampTZ: - return new DateTime.utc(2000) - .add(new Duration(microseconds: value.getInt64(0))); - - case TypeDate: - return new DateTime.utc(2000) - .add(new Duration(days: value.getInt32(0))); - - case TypeJSONB: { - // Removes version which is first character and currently always '1' - var string = UTF8.decode(value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1)); - return JSON.decode(string); - } - - case TypeByteArray: - return value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes); - - default: - return UTF8.decode( - value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); - } - } -} diff --git a/lib/src/query.dart b/lib/src/query.dart index 16e3fa1..93a986a 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'package:postgres/src/binary_codec.dart'; import 'package:postgres/src/execution_context.dart'; -import 'postgresql_codec.dart'; +import 'package:postgres/src/text_codec.dart'; +import 'types.dart'; import 'connection.dart'; import 'dart:io'; import 'substituter.dart'; @@ -24,7 +26,7 @@ class Query { final PostgreSQLExecutionContext transaction; final PostgreSQLConnection connection; - List specifiedParameterTypeCodes; + List specifiedParameterTypeCodes; List _fieldDescriptions; @@ -63,9 +65,9 @@ class Query { return "\$$index"; }); - specifiedParameterTypeCodes = formatIdentifiers.map((i) => i.typeCode).toList(); + specifiedParameterTypeCodes = formatIdentifiers.map((i) => i.type).toList(); - var parameterList = formatIdentifiers.map((id) => encodeParameter(id, substitutionValues)).toList(); + var parameterList = formatIdentifiers.map((id) => new ParameterValue(id, substitutionValues)).toList(); var messages = [ new ParseMessage(sqlString, statementName: statementName), @@ -85,7 +87,7 @@ class Query { void sendCachedQuery(Socket socket, CachedQuery cacheQuery, Map substitutionValues) { var statementName = cacheQuery.preparedStatementName; var parameterList = - cacheQuery.orderedParameters.map((identifier) => encodeParameter(identifier, substitutionValues)).toList(); + cacheQuery.orderedParameters.map((identifier) => new ParameterValue(identifier, substitutionValues)).toList(); var bytes = ClientMessage.aggregateBytes( [new BindMessage(parameterList, statementName: statementName), new ExecuteMessage(), new SyncMessage()]); @@ -93,19 +95,17 @@ class Query { socket.add(bytes); } - ParameterValue encodeParameter(PostgreSQLFormatIdentifier identifier, Map substitutionValues) { - if (identifier.typeCode != null) { - return new ParameterValue.binary(substitutionValues[identifier.name], identifier.typeCode); - } else { - return new ParameterValue.text(substitutionValues[identifier.name]); - } - } - PostgreSQLException validateParameters(List parameterTypeIDs) { var actualParameterTypeCodeIterator = parameterTypeIDs.iterator; - var parametersAreMismatched = specifiedParameterTypeCodes.map((specifiedTypeCode) { + var parametersAreMismatched = specifiedParameterTypeCodes.map((specifiedType) { actualParameterTypeCodeIterator.moveNext(); - return actualParameterTypeCodeIterator.current == (specifiedTypeCode ?? actualParameterTypeCodeIterator.current); + + if (specifiedType == null) { + return true; + } + + final actualType = PostgresBinaryDecoder.typeMap[actualParameterTypeCodeIterator.current]; + return actualType == specifiedType; }).any((v) => v == false); if (parametersAreMismatched) { @@ -125,7 +125,7 @@ class Query { var lazyDecodedData = rawRowData.map((bd) { iterator.moveNext(); - return PostgreSQLCodec.decodeValue(bd, iterator.current.typeID); + return iterator.current.converter.convert(bd?.buffer?.asUint8List(bd.offsetInBytes, bd.lengthInBytes)); }); rows.add(lazyDecodedData.toList()); @@ -160,27 +160,36 @@ class CachedQuery { } class ParameterValue { - ParameterValue.binary(dynamic value, this.postgresType) { - isBinary = true; - bytes = PostgreSQLCodec.encodeBinary(value, this.postgresType)?.buffer?.asUint8List(); + factory ParameterValue(PostgreSQLFormatIdentifier identifier, Map substitutionValues) { + if (identifier.type == null) { + return new ParameterValue.text(substitutionValues[identifier.name]); + } + + return new ParameterValue.binary(substitutionValues[identifier.name], identifier.type); + } + + ParameterValue.binary(dynamic value, PostgreSQLDataType postgresType) : isBinary = true { + final converter = new PostgresBinaryEncoder(postgresType); + bytes = converter.convert(value); length = bytes?.length ?? 0; } - ParameterValue.text(dynamic value) { - isBinary = false; + ParameterValue.text(dynamic value) : isBinary = false { if (value != null) { - bytes = UTF8.encode(PostgreSQLCodec.encode(value, escapeStrings: false)); + final converter = new PostgresTextEncoder(false); + bytes = UTF8.encode(converter.convert(value)); } length = bytes?.length; } - bool isBinary; - int postgresType; + final bool isBinary; Uint8List bytes; int length; } class FieldDescription { + Converter converter; + String fieldName; int tableID; int columnID; @@ -218,6 +227,8 @@ class FieldDescription { formatCode = byteData.getUint16(offset); offset += 2; + converter = new PostgresBinaryDecoder(typeID); + return offset; } @@ -238,24 +249,23 @@ class PostgreSQLFormatToken { } class PostgreSQLFormatIdentifier { - static Map typeStringToCodeMap = { - "text": PostgreSQLCodec.TypeText, - "int2": PostgreSQLCodec.TypeInt2, - "int4": PostgreSQLCodec.TypeInt4, - "int8": PostgreSQLCodec.TypeInt8, - "float4": PostgreSQLCodec.TypeFloat4, - "float8": PostgreSQLCodec.TypeFloat8, - "boolean": PostgreSQLCodec.TypeBool, - "date": PostgreSQLCodec.TypeDate, - "timestamp": PostgreSQLCodec.TypeTimestamp, - "timestamptz": PostgreSQLCodec.TypeTimestampTZ, - "jsonb": PostgreSQLCodec.TypeJSONB + static Map typeStringToCodeMap = { + "text": PostgreSQLDataType.text, + "serial": PostgreSQLDataType.serial, + "bigserial": PostgreSQLDataType.bigSerial, + "int2": PostgreSQLDataType.smallInteger, + "int4": PostgreSQLDataType.integer, + "int8": PostgreSQLDataType.bigInteger, + "float4": PostgreSQLDataType.real, + "float8": PostgreSQLDataType.double, + "boolean": PostgreSQLDataType.boolean, + "date": PostgreSQLDataType.date, + "timestamp": PostgreSQLDataType.timestampWithoutTimezone, + "timestamptz": PostgreSQLDataType.timestampWithTimezone, + "jsonb": PostgreSQLDataType.json, + "bytea": PostgreSQLDataType.byteArray }; - static int postgresCodeForDataTypeString(String dt) { - return typeStringToCodeMap[dt]; - } - PostgreSQLFormatIdentifier(String t) { var components = t.split("::"); if (components.length > 1) { @@ -270,8 +280,8 @@ class PostgreSQLFormatIdentifier { var dataTypeString = variableComponents.last; if (dataTypeString != null) { - typeCode = postgresCodeForDataTypeString(dataTypeString); - if (typeCode == null) { + type = typeStringToCodeMap[dataTypeString]; + if (type == null) { throw new FormatException("Invalid type code in substitution variable '$t'"); } } @@ -285,6 +295,6 @@ class PostgreSQLFormatIdentifier { } String name; - int typeCode; + PostgreSQLDataType type; String typeCast; } diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index d40f529..ae914d1 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -11,8 +11,7 @@ class ErrorResponseMessage implements ServerMessage { List fields = [new ErrorField()]; void readBytes(Uint8List bytes) { - var lastByteRemovedList = - new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); + var lastByteRemovedList = new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); lastByteRemovedList.forEach((byte) { if (byte != 0) { @@ -58,8 +57,7 @@ class ParameterStatusMessage extends ServerMessage { void readBytes(Uint8List bytes) { name = UTF8.decode(bytes.sublist(0, bytes.indexOf(0))); - value = - UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + value = UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } } @@ -122,8 +120,7 @@ class DataRowMessage extends ServerMessage { } else if (dataSize == -1) { values.add(null); } else { - var rawBytes = new ByteData.view( - bytes.buffer, bytes.offsetInBytes + offset, dataSize); + var rawBytes = new ByteData.view(bytes.buffer, bytes.offsetInBytes + offset, dataSize); values.add(rawBytes); offset += dataSize; } @@ -208,6 +205,11 @@ class UnknownMessage extends ServerMessage { this.bytes = bytes; } + @override + int get hashCode { + return bytes.hashCode; + } + @override operator ==(dynamic other) { if (bytes != null) { @@ -282,4 +284,4 @@ class ErrorField { _buffer.writeCharCode(byte); } } -} \ No newline at end of file +} diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 4ddb3dc..33f4123 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -1,4 +1,5 @@ -import 'postgresql_codec.dart'; +import 'package:postgres/src/text_codec.dart'; +import 'types.dart'; import 'query.dart'; class PostgreSQLFormat { @@ -40,6 +41,8 @@ class PostgreSQLFormat { return "date"; case PostgreSQLDataType.json: return "jsonb"; + case PostgreSQLDataType.byteArray: + return "bytea"; } return null; @@ -47,8 +50,9 @@ class PostgreSQLFormat { static String substitute(String fmtString, Map values, {SQLReplaceIdentifierFunction replace: null}) { + final converter = new PostgresTextEncoder(true); values ??= {}; - replace ??= (spec, index) => PostgreSQLCodec.encode(values[spec.name]); + replace ??= (spec, index) => converter.convert(values[spec.name]); var items = []; PostgreSQLFormatToken currentPtr = null; diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart new file mode 100644 index 0000000..78a29e0 --- /dev/null +++ b/lib/src/text_codec.dart @@ -0,0 +1,160 @@ +import 'dart:convert'; + +import 'package:postgres/postgres.dart'; + +class PostgresTextEncoder extends Converter { + const PostgresTextEncoder(this.escapeStrings); + + final bool escapeStrings; + + @override + String convert(dynamic value) { + if (value == null) { + return "null"; + } + + if (value is int) { + return encodeNumber(value); + } + + if (value is double) { + return encodeDouble(value); + } + + if (value is String) { + return encodeString(value, escapeStrings); + } + + if (value is DateTime) { + return encodeDateTime(value, isDateOnly: false); + } + + if (value is bool) { + return encodeBoolean(value); + } + + if (value is Map) { + return encodeJSON(value); + } + + throw new PostgreSQLException("Could not infer type of value '$value'."); + } + + String encodeString(String text, bool escapeStrings) { + if (!escapeStrings) { + return text; + } + + var backslashCodeUnit = r"\".codeUnitAt(0); + var quoteCodeUnit = r"'".codeUnitAt(0); + + var quoteCount = 0; + var backslashCount = 0; + var it = new RuneIterator(text); + while (it.moveNext()) { + if (it.current == backslashCodeUnit) { + backslashCount++; + } else if (it.current == quoteCodeUnit) { + quoteCount++; + } + } + + var buf = new StringBuffer(); + + if (backslashCount > 0) { + buf.write(" E"); + } + + buf.write("'"); + + if (quoteCount == 0 && backslashCount == 0) { + buf.write(text); + } else { + text.codeUnits.forEach((i) { + if (i == quoteCodeUnit || i == backslashCodeUnit) { + buf.writeCharCode(i); + buf.writeCharCode(i); + } else { + buf.writeCharCode(i); + } + }); + } + + buf.write("'"); + + return buf.toString(); + } + + String encodeNumber(num value) { + if (value.isNaN) { + return "'nan'"; + } + + if (value.isInfinite) { + return value.isNegative ? "'-infinity'" : "'infinity'"; + } + + return value.toInt().toString(); + } + + String encodeDouble(double value) { + if (value.isNaN) { + return "'nan'"; + } + + if (value.isInfinite) { + return value.isNegative ? "'-infinity'" : "'infinity'"; + } + + return value.toString(); + } + + String encodeBoolean(bool value) { + return value ? "TRUE" : "FALSE"; + } + + String encodeDateTime(DateTime value, {bool isDateOnly: false}) { + var string = value.toIso8601String(); + + if (isDateOnly) { + string = string.split("T").first; + } else { + if (!value.isUtc) { + var timezoneHourOffset = value.timeZoneOffset.inHours; + var timezoneMinuteOffset = value.timeZoneOffset.inMinutes % 60; + + var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, "0"); + var minuteComponent = timezoneMinuteOffset.abs().toString().padLeft(2, "0"); + + if (timezoneHourOffset >= 0) { + hourComponent = "+${hourComponent}"; + } else { + hourComponent = "-${hourComponent}"; + } + + var timezoneString = [hourComponent, minuteComponent].join(":"); + string = [string, timezoneString].join(""); + } + } + + if (string.substring(0, 1) == "-") { + string = string.substring(1) + " BC"; + } else if (string.substring(0, 1) == "+") { + string = string.substring(1); + } + + return "'$string'"; + } + + String encodeJSON(dynamic value) { + if (value == null) { + return "null"; + } + + if (value is String) { + return "'${JSON.encode(value)}'"; + } + + return "${JSON.encode(value)}"; + } +} diff --git a/lib/src/types.dart b/lib/src/types.dart new file mode 100644 index 0000000..9f62fcc --- /dev/null +++ b/lib/src/types.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +/* + Adding a new type: + + 1. add item to this enumeration + 2. update all switch statements on this type + 3. add pg type code -> enumeration item in PostgresBinaryDecoder.typeMap (lookup type code: https://doxygen.postgresql.org/include_2catalog_2pg__type_8h_source.html) + 4. add identifying key to PostgreSQLFormatIdentifier.typeStringToCodeMap. + */ + +/// Supported data types. +enum PostgreSQLDataType { + /// Must be a [String]. + text, + + /// Must be an [int] (4-byte integer) + integer, + + /// Must be an [int] (2-byte integer) + smallInteger, + + /// Must be an [int] (8-byte integer) + bigInteger, + + /// Must be an [int] (autoincrementing 4-byte integer) + serial, + + /// Must be an [int] (autoincrementing 8-byte integer) + bigSerial, + + /// Must be a [double] (32-bit floating point value) + real, + + /// Must be a [double] (64-bit floating point value) + double, + + /// Must be a [bool] + boolean, + + /// Must be a [DateTime] (microsecond date and time precision) + timestampWithoutTimezone, + + /// Must be a [DateTime] (microsecond date and time precision) + timestampWithTimezone, + + /// Must be a [DateTime] (contains year, month and day only) + date, + + /// Must be encodable via [JSON.encode]. + /// + /// Values will be encoded via [JSON.encode] before being sent to the database. + json, + + /// Must be a [List]. + /// + /// Each element of the list must fit into a byte (0-255). + byteArray +} \ No newline at end of file diff --git a/test/encoding_test.dart b/test/encoding_test.dart index be8ae70..6676022 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -1,188 +1,311 @@ +import 'dart:async'; import 'dart:convert'; +import 'package:postgres/postgres.dart'; +import 'package:postgres/src/binary_codec.dart'; +import 'package:postgres/src/text_codec.dart'; import 'package:test/test.dart'; -import 'dart:typed_data'; -import 'package:postgres/src/postgresql_codec.dart'; +import 'package:postgres/src/types.dart'; import 'package:postgres/src/utf8_backed_string.dart'; +PostgreSQLConnection conn; + void main() { - test("Binary encode/decode inverse", () { - expectInverse(true, PostgreSQLCodec.TypeBool); - expectInverse(false, PostgreSQLCodec.TypeBool); - - expectInverse(-1, PostgreSQLCodec.TypeInt2); - expectInverse(0, PostgreSQLCodec.TypeInt2); - expectInverse(1, PostgreSQLCodec.TypeInt2); - - expectInverse(-1, PostgreSQLCodec.TypeInt4); - expectInverse(0, PostgreSQLCodec.TypeInt4); - expectInverse(1, PostgreSQLCodec.TypeInt4); - - expectInverse(-1, PostgreSQLCodec.TypeInt8); - expectInverse(0, PostgreSQLCodec.TypeInt8); - expectInverse(1, PostgreSQLCodec.TypeInt8); - - expectInverse("", PostgreSQLCodec.TypeText); - expectInverse("foo", PostgreSQLCodec.TypeText); - expectInverse("foo\n", PostgreSQLCodec.TypeText); - expectInverse("foo\nbar;s", PostgreSQLCodec.TypeText); - - expectInverse(-1.0, PostgreSQLCodec.TypeFloat4); - expectInverse(0.0, PostgreSQLCodec.TypeFloat4); - expectInverse(1.0, PostgreSQLCodec.TypeFloat4); - - expectInverse(-1.0, PostgreSQLCodec.TypeFloat8); - expectInverse(0.0, PostgreSQLCodec.TypeFloat8); - expectInverse(1.0, PostgreSQLCodec.TypeFloat8); - - expectInverse(new DateTime.utc(2016, 10, 1), PostgreSQLCodec.TypeDate); - expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLCodec.TypeDate); - expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLCodec.TypeDate); - - expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLCodec.TypeTimestamp); - expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLCodec.TypeTimestamp); - - expectInverse( - new DateTime.utc(1920, 10, 1), PostgreSQLCodec.TypeTimestampTZ); - expectInverse( - new DateTime.utc(2120, 10, 5), PostgreSQLCodec.TypeTimestampTZ); - }); + group("Binary encoders", () { + setUp(() async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + await conn.open(); + }); - test("Escape strings", () { - // ' b o b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bob')), - equals([39, 98, 111, 98, 39])); + tearDown(() async { + await conn.close(); + conn = null; + }); - // ' b o \n b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bo\nb')), - equals([39, 98, 111, 10, 98, 39])); + // expectInverse ensures that: + // 1. encoder/decoder is reversible + // 2. can actually encode and decode a real pg query + // it also creates a table named t with column v of type being tested + test("bool", () async { + await expectInverse(true, PostgreSQLDataType.boolean); + await expectInverse(false, PostgreSQLDataType.boolean); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:boolean)", substitutionValues: {"v": "not-bool"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: bool")); + } + }); - // ' b o \r b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bo\rb')), - equals([39, 98, 111, 13, 98, 39])); + test("smallint", () async { + await expectInverse(-1, PostgreSQLDataType.smallInteger); + await expectInverse(0, PostgreSQLDataType.smallInteger); + await expectInverse(1, PostgreSQLDataType.smallInteger); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:int2)", substitutionValues: {"v": "not-int2"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: int")); + } + }); - // ' b o \b b ' - expect(UTF8.encode(PostgreSQLCodec.encode('bo\bb')), - equals([39, 98, 111, 8, 98, 39])); + test("integer", () async { + await expectInverse(-1, PostgreSQLDataType.integer); + await expectInverse(0, PostgreSQLDataType.integer); + await expectInverse(1, PostgreSQLDataType.integer); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:int4)", substitutionValues: {"v": "not-int4"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: int")); + } + }); - // ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("'")), equals([39, 39, 39, 39])); + test("serial", () async { + await expectInverse(0, PostgreSQLDataType.serial); + await expectInverse(1, PostgreSQLDataType.serial); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:serial)", substitutionValues: {"v": "not-serial"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: int")); + } - // ' ' ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("''")), - equals([39, 39, 39, 39, 39, 39])); + }); - // ' ' ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("\''")), - equals([39, 39, 39, 39, 39, 39])); + test("bigint", () async { + await expectInverse(-1, PostgreSQLDataType.bigInteger); + await expectInverse(0, PostgreSQLDataType.bigInteger); + await expectInverse(1, PostgreSQLDataType.bigInteger); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:int8)", substitutionValues: {"v": "not-int8"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: int")); + } - // sp E ' \ \ ' ' ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("\\''")), - equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + }); - // sp E ' \ \ ' ' ' - expect(UTF8.encode(PostgreSQLCodec.encode("\\'")), - equals([32, 69, 39, 92, 92, 39, 39, 39])); - }); + test("bigserial", () async { + await expectInverse(0, PostgreSQLDataType.bigSerial); + await expectInverse(1, PostgreSQLDataType.bigSerial); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:bigserial)", substitutionValues: {"v": "not-bigserial"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: int")); + } - test("Encode DateTime", () { - // Get users current timezone - var tz = new DateTime(2001, 2, 3).timeZoneOffset; - var tzOffsetDelimiter = "${tz.isNegative ? '-' : '+'}" - "${tz.abs().inHours.toString().padLeft(2, '0')}" - ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; - - var pairs = { - "2001-02-03T00:00:00.000$tzOffsetDelimiter": - new DateTime(2001, DateTime.FEBRUARY, 3), - "2001-02-03T04:05:06.000$tzOffsetDelimiter": - new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "2001-02-03T04:05:06.999$tzOffsetDelimiter": - new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), - "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": - new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), - "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": - new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": - new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter": - new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0) - }; - - pairs.forEach((k, v) { - expect(PostgreSQLCodec.encode(v), "'$k'"); }); - }); - test("Encode Double", () { - var pairs = { - "'nan'": double.NAN, - "'infinity'": double.INFINITY, - "'-infinity'": double.NEGATIVE_INFINITY, - "1.7976931348623157e+308": double.MAX_FINITE, - "5e-324": double.MIN_POSITIVE, - "-0.0": -0.0, - "0.0": 0.0 - }; - - pairs.forEach((k, v) { - expect(PostgreSQLCodec.encode(v), "$k"); - expect( - PostgreSQLCodec.encode(v, dataType: PostgreSQLDataType.real), "$k"); - expect( - PostgreSQLCodec.encode(v, dataType: PostgreSQLDataType.double), "$k"); - }); - - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.double), "1"); - - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.real), - "null"); - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.double), - "null"); - }); + test("text", () async { + await expectInverse("", PostgreSQLDataType.text); + await expectInverse("foo", PostgreSQLDataType.text); + await expectInverse("foo\n", PostgreSQLDataType.text); + await expectInverse("foo\nbar;s", PostgreSQLDataType.text); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:text)", substitutionValues: {"v": 0}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: String")); + } + }); - test("Encode Int", () { - expect( - PostgreSQLCodec.encode(1.0, dataType: PostgreSQLDataType.integer), "1"); - - expect(PostgreSQLCodec.encode(1), "1"); - expect( - PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.integer), "1"); - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.bigInteger), - "1"); - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.smallInteger), - "1"); - - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.integer), - "null"); - expect( - PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.bigInteger), - "null"); - expect( - PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.smallInteger), - "null"); - }); + test("real", () async { + await expectInverse(-1.0, PostgreSQLDataType.real); + await expectInverse(0.0, PostgreSQLDataType.real); + await expectInverse(1.0, PostgreSQLDataType.real); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:float4)", substitutionValues: {"v": "not-real"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: double")); + } + }); + + test("double", () async { + await expectInverse(-1.0, PostgreSQLDataType.double); + await expectInverse(0.0, PostgreSQLDataType.double); + await expectInverse(1.0, PostgreSQLDataType.double); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:float8)", substitutionValues: {"v": "not-double"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: double")); + } + }); + + test("date", () async { + await expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLDataType.date); + await expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLDataType.date); + await expectInverse(new DateTime.utc(2016, 10, 1), PostgreSQLDataType.date); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:date)", substitutionValues: {"v": "not-date"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: DateTime")); + } + }); + + test("timestamp", () async { + await expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLDataType.timestampWithoutTimezone); + await expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLDataType.timestampWithoutTimezone); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:timestamp)", substitutionValues: {"v": "not-timestamp"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: DateTime")); + } + }); - test("Encode Bool", () { - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.boolean), - "null"); - expect(PostgreSQLCodec.encode(true), "TRUE"); - expect(PostgreSQLCodec.encode(false), "FALSE"); - expect(PostgreSQLCodec.encode(true, dataType: PostgreSQLDataType.boolean), - "TRUE"); - expect(PostgreSQLCodec.encode(false, dataType: PostgreSQLDataType.boolean), - "FALSE"); + test("timestamptz", () async { + await expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLDataType.timestampWithTimezone); + await expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLDataType.timestampWithTimezone); + try { + await conn.query("INSERT INTO t (v) VALUES (@v:timestamptz)", substitutionValues: {"v": "not-timestamptz"}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: DateTime")); + } + + }); + + test("jsonb", () async { + await expectInverse("string", PostgreSQLDataType.json); + await expectInverse(2, PostgreSQLDataType.json); + await expectInverse(["foo"], PostgreSQLDataType.json); + await expectInverse({ + "key": "val", + "key1": 1, + "array": ["foo"] + }, PostgreSQLDataType.json); + + try { + await conn.query("INSERT INTO t (v) VALUES (@v:jsonb)", substitutionValues: {"v": new DateTime.now()}); + fail('unreachable'); + } on JsonUnsupportedObjectError catch (e) {} + }); + + test("bytea", () async { + await expectInverse([0], PostgreSQLDataType.byteArray); + await expectInverse([1,2,3,4,5], PostgreSQLDataType.byteArray); + await expectInverse([255, 254, 253], PostgreSQLDataType.byteArray); + + try { + await conn.query("INSERT INTO t (v) VALUES (@v:bytea)", substitutionValues: {"v": new DateTime.now()}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: List")); + } + }); }); + group("Text encoders", () { + test("Escape strings", () { + final encoder = new PostgresTextEncoder(true); + // ' b o b ' + expect(UTF8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); + + // ' b o \n b ' + expect(UTF8.encode(encoder.convert('bo\nb')), equals([39, 98, 111, 10, 98, 39])); - test("Encode JSONB", () { - expect(PostgreSQLCodec.encode(null, dataType: PostgreSQLDataType.json), "null"); - expect(PostgreSQLCodec.encode("a", dataType: PostgreSQLDataType.json), "'\"a\"'"); - expect(PostgreSQLCodec.encode(1, dataType: PostgreSQLDataType.json), "1"); - expect(PostgreSQLCodec.encode(2.0, dataType: PostgreSQLDataType.json), "2.0"); - expect(PostgreSQLCodec.encode({"a":"b"}, dataType: PostgreSQLDataType.json), "{\"a\":\"b\"}"); - expect(PostgreSQLCodec.encode([{"a":"b"}], dataType: PostgreSQLDataType.json), "[{\"a\":\"b\"}]"); - expect(PostgreSQLCodec.encode({"a":true}, dataType: PostgreSQLDataType.json), "{\"a\":true}"); - expect(PostgreSQLCodec.encode({"b":false}, dataType: PostgreSQLDataType.json), "{\"b\":false}"); + // ' b o \r b ' + expect(UTF8.encode(encoder.convert('bo\rb')), equals([39, 98, 111, 13, 98, 39])); + + // ' b o \b b ' + expect(UTF8.encode(encoder.convert('bo\bb')), equals([39, 98, 111, 8, 98, 39])); + + // ' ' ' ' + expect(UTF8.encode(encoder.convert("'")), equals([39, 39, 39, 39])); + + // ' ' ' ' ' ' + expect(UTF8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); + + // ' ' ' ' ' ' + expect(UTF8.encode(encoder.convert("\''")), equals([39, 39, 39, 39, 39, 39])); + + // sp E ' \ \ ' ' ' ' ' + expect(UTF8.encode(encoder.convert("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + + // sp E ' \ \ ' ' ' + expect(UTF8.encode(encoder.convert("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); + }); + + test("Encode DateTime", () { + // Get users current timezone + var tz = new DateTime(2001, 2, 3).timeZoneOffset; + var tzOffsetDelimiter = "${tz.isNegative ? '-' : '+'}" + "${tz + .abs() + .inHours + .toString() + .padLeft(2, '0')}" + ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; + + var pairs = { + "2001-02-03T00:00:00.000$tzOffsetDelimiter": new DateTime(2001, DateTime.FEBRUARY, 3), + "2001-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), + "2001-02-03T04:05:06.999$tzOffsetDelimiter": new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), + "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), + "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0) + }; + + final encoder = new PostgresTextEncoder(false); + pairs.forEach((k, v) { + expect(encoder.convert(v), "'$k'"); + }); + }); + + test("Encode Double", () { + var pairs = { + "'nan'": double.NAN, + "'infinity'": double.INFINITY, + "'-infinity'": double.NEGATIVE_INFINITY, + "1.7976931348623157e+308": double.MAX_FINITE, + "5e-324": double.MIN_POSITIVE, + "-0.0": -0.0, + "0.0": 0.0 + }; + + final encoder = new PostgresTextEncoder(false); + pairs.forEach((k, v) { + expect(encoder.convert(v), "$k"); + }); + }); + + test("Encode Int", () { + final encoder = new PostgresTextEncoder(false); + + expect(encoder.convert(1), "1"); + expect(encoder.convert(1234324323), "1234324323"); + expect(encoder.convert(-1234324323), "-1234324323"); + }); + + test("Encode Bool", () { + final encoder = new PostgresTextEncoder(false); + + expect(encoder.convert(true), "TRUE"); + expect(encoder.convert(false), "FALSE"); + }); + + test("Encode JSONB", () { + final encoder = new 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 = new PostgresTextEncoder(false); + try { + encoder.convert([]); + fail('unreachable'); + } on PostgreSQLException catch (e) { + expect(e.toString(), contains("Could not infer type")); + } + }); }); test("UTF8String caches string regardless of which method is called first", () { @@ -197,9 +320,32 @@ void main() { }); } -expectInverse(dynamic value, int dataType) { - var encodedValue = PostgreSQLCodec.encodeBinary(value, dataType); - var decodedValue = PostgreSQLCodec.decodeValue( - new ByteData.view(encodedValue.buffer), dataType); +Future expectInverse(dynamic value, PostgreSQLDataType dataType) async { + final type = PostgreSQLFormat.dataTypeStringForDataType(dataType); + + await conn.execute("CREATE TEMPORARY TABLE IF NOT EXISTS t (v $type)"); + final result = await conn.query("INSERT INTO t (v) VALUES (${PostgreSQLFormat.id("v", type: dataType)}) RETURNING v", substitutionValues: { + "v": value + }); + expect(result.first.first, equals(value)); + + final encoder = new PostgresBinaryEncoder(dataType); + final encodedValue = encoder.convert(value); + + if (dataType == PostgreSQLDataType.serial) { + dataType = PostgreSQLDataType.integer; + } else if (dataType == PostgreSQLDataType.bigSerial) { + dataType = PostgreSQLDataType.bigInteger; + } + var code; + PostgresBinaryDecoder.typeMap.forEach((key, type) { + if (type == dataType) { + code = key; + } + }); + + final decoder = new PostgresBinaryDecoder(code); + final decodedValue = decoder.convert(encodedValue); + expect(decodedValue, value); } diff --git a/test/query_test.dart b/test/query_test.dart index f90943e..cfb64cd 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -1,6 +1,6 @@ import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; -import 'package:postgres/src/postgresql_codec.dart'; +import 'package:postgres/src/types.dart'; void main() { group("Successful queries", () { From fa441aa8d794c2d8190d4345396fdd00f0b09018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 5 Apr 2018 19:05:43 +0200 Subject: [PATCH 29/73] Use package:dart2_constant to polyfill Dart2 support. (#42) --- lib/src/connection.dart | 7 ++--- lib/src/query.dart | 11 +++++--- lib/src/server_messages.dart | 16 ++++++----- lib/src/utf8_backed_string.dart | 6 ++--- pubspec.yaml | 1 + test/encoding_test.dart | 48 ++++++++++++++++++--------------- test/interpolation_test.dart | 7 ++--- 7 files changed, 54 insertions(+), 42 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 89d3c5a..14b5f4d 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -1,9 +1,11 @@ library postgres.connection; import 'dart:async'; -import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; +import 'package:dart2_constant/convert.dart' as convert; + import 'package:postgres/src/query_cache.dart'; import 'package:postgres/src/execution_context.dart'; import 'package:postgres/src/query_queue.dart'; @@ -12,7 +14,6 @@ import 'message_window.dart'; import 'query.dart'; import 'server_messages.dart'; -import 'dart:io'; import 'client_messages.dart'; part 'connection_fsm.dart'; @@ -407,7 +408,7 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo orderedTableNames.forEach((tableName) { iterator.moveNext(); if (tableName.first != null) { - _tableOIDNameMap[iterator.current] = UTF8.decode(tableName.first); + _tableOIDNameMap[iterator.current] = convert.utf8.decode(tableName.first); } }); } diff --git a/lib/src/query.dart b/lib/src/query.dart index 93a986a..71400c7 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -1,15 +1,18 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dart2_constant/convert.dart' as convert; + import 'package:postgres/src/binary_codec.dart'; import 'package:postgres/src/execution_context.dart'; import 'package:postgres/src/text_codec.dart'; import 'types.dart'; import 'connection.dart'; -import 'dart:io'; import 'substituter.dart'; import 'client_messages.dart'; -import 'dart:typed_data'; -import 'dart:convert'; class Query { Query(this.statement, this.substitutionValues, this.connection, this.transaction); @@ -177,7 +180,7 @@ class ParameterValue { ParameterValue.text(dynamic value) : isBinary = false { if (value != null) { final converter = new PostgresTextEncoder(false); - bytes = UTF8.encode(converter.convert(value)); + bytes = convert.utf8.encode(converter.convert(value)); } length = bytes?.length; } diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index ae914d1..3d777ca 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -1,5 +1,7 @@ import 'dart:typed_data'; -import 'dart:convert'; + +import 'package:dart2_constant/convert.dart' as convert; + import 'connection.dart'; import 'query.dart'; @@ -56,8 +58,8 @@ class ParameterStatusMessage extends ServerMessage { String value; void readBytes(Uint8List bytes) { - name = UTF8.decode(bytes.sublist(0, bytes.indexOf(0))); - value = UTF8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + name = convert.utf8.decode(bytes.sublist(0, bytes.indexOf(0))); + value = convert.utf8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } } @@ -69,7 +71,7 @@ class ReadyForQueryMessage extends ServerMessage { String state; void readBytes(Uint8List bytes) { - state = UTF8.decode(bytes); + state = convert.utf8.decode(bytes); } } @@ -138,8 +140,8 @@ class NotificationResponseMessage extends ServerMessage { void readBytes(Uint8List bytes) { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); processID = view.getUint32(0); - channel = UTF8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); - payload = UTF8.decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); + channel = convert.utf8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); + payload = convert.utf8.decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); } } @@ -149,7 +151,7 @@ class CommandCompleteMessage extends ServerMessage { static RegExp identifierExpression = new RegExp(r"[A-Z ]*"); void readBytes(Uint8List bytes) { - var str = UTF8.decode(bytes.sublist(0, bytes.length - 1)); + var str = convert.utf8.decode(bytes.sublist(0, bytes.length - 1)); var match = identifierExpression.firstMatch(str); if (match.end < str.length) { diff --git a/lib/src/utf8_backed_string.dart b/lib/src/utf8_backed_string.dart index e1b96dc..6d9da61 100644 --- a/lib/src/utf8_backed_string.dart +++ b/lib/src/utf8_backed_string.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'package:dart2_constant/convert.dart' as convert; class UTF8BackedString { UTF8BackedString(this.string); @@ -11,14 +11,14 @@ class UTF8BackedString { int get utf8Length { if (_cachedUTF8Bytes == null) { - _cachedUTF8Bytes = UTF8.encode(string); + _cachedUTF8Bytes = convert.utf8.encode(string); } return _cachedUTF8Bytes.length; } List get utf8Bytes { if (_cachedUTF8Bytes == null) { - _cachedUTF8Bytes = UTF8.encode(string); + _cachedUTF8Bytes = convert.utf8.encode(string); } return _cachedUTF8Bytes; } diff --git a/pubspec.yaml b/pubspec.yaml index b19378c..d4f1a95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: crypto: ^2.0.0 + dart2_constant: ^1.0.0 dev_dependencies: test: '>=0.12.0 <0.13.0' diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 6676022..3b3602b 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -1,9 +1,13 @@ import 'dart:async'; import 'dart:convert'; + +import 'package:dart2_constant/convert.dart' as convert; +import 'package:dart2_constant/core.dart' as core; +import 'package:test/test.dart'; + import 'package:postgres/postgres.dart'; import 'package:postgres/src/binary_codec.dart'; import 'package:postgres/src/text_codec.dart'; -import 'package:test/test.dart'; import 'package:postgres/src/types.dart'; import 'package:postgres/src/utf8_backed_string.dart'; @@ -203,31 +207,31 @@ void main() { test("Escape strings", () { final encoder = new PostgresTextEncoder(true); // ' b o b ' - expect(UTF8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); + expect(convert.utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); // ' b o \n b ' - expect(UTF8.encode(encoder.convert('bo\nb')), equals([39, 98, 111, 10, 98, 39])); + expect(convert.utf8.encode(encoder.convert('bo\nb')), equals([39, 98, 111, 10, 98, 39])); // ' b o \r b ' - expect(UTF8.encode(encoder.convert('bo\rb')), equals([39, 98, 111, 13, 98, 39])); + expect(convert.utf8.encode(encoder.convert('bo\rb')), equals([39, 98, 111, 13, 98, 39])); // ' b o \b b ' - expect(UTF8.encode(encoder.convert('bo\bb')), equals([39, 98, 111, 8, 98, 39])); + expect(convert.utf8.encode(encoder.convert('bo\bb')), equals([39, 98, 111, 8, 98, 39])); // ' ' ' ' - expect(UTF8.encode(encoder.convert("'")), equals([39, 39, 39, 39])); + expect(convert.utf8.encode(encoder.convert("'")), equals([39, 39, 39, 39])); // ' ' ' ' ' ' - expect(UTF8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); + expect(convert.utf8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); // ' ' ' ' ' ' - expect(UTF8.encode(encoder.convert("\''")), equals([39, 39, 39, 39, 39, 39])); + expect(convert.utf8.encode(encoder.convert("\''")), equals([39, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' ' ' - expect(UTF8.encode(encoder.convert("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + expect(convert.utf8.encode(encoder.convert("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' - expect(UTF8.encode(encoder.convert("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); + expect(convert.utf8.encode(encoder.convert("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); }); test("Encode DateTime", () { @@ -242,13 +246,13 @@ void main() { ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; var pairs = { - "2001-02-03T00:00:00.000$tzOffsetDelimiter": new DateTime(2001, DateTime.FEBRUARY, 3), - "2001-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "2001-02-03T04:05:06.999$tzOffsetDelimiter": new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), - "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), - "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0) + "2001-02-03T00:00:00.000$tzOffsetDelimiter": new DateTime(2001, core.DateTime.february, 3), + "2001-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(2001, core.DateTime.february, 3, 4, 5, 6, 0), + "2001-02-03T04:05:06.999$tzOffsetDelimiter": new DateTime(2001, core.DateTime.february, 3, 4, 5, 6, 999), + "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": new DateTime(-10, core.DateTime.february, 3, 4, 5, 6, 123), + "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-10, core.DateTime.february, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-12345, core.DateTime.february, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(12345, core.DateTime.february, 3, 4, 5, 6, 0) }; final encoder = new PostgresTextEncoder(false); @@ -259,11 +263,11 @@ void main() { test("Encode Double", () { var pairs = { - "'nan'": double.NAN, - "'infinity'": double.INFINITY, - "'-infinity'": double.NEGATIVE_INFINITY, - "1.7976931348623157e+308": double.MAX_FINITE, - "5e-324": double.MIN_POSITIVE, + "'nan'": core.double.nan, + "'infinity'": core.double.infinity, + "'-infinity'": core.double.negativeInfinity, + "1.7976931348623157e+308": core.double.maxFinite, + "5e-324": core.double.minPositive, "-0.0": -0.0, "0.0": 0.0 }; diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index a134588..84f2d49 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -1,7 +1,8 @@ -import 'dart:convert'; -import 'package:postgres/src/substituter.dart'; +import 'package:dart2_constant/convert.dart' as convert; import 'package:test/test.dart'; +import 'package:postgres/src/substituter.dart'; + void main() { test("Simple replacement", () { var result = PostgreSQLFormat.substitute("@id", {"id": 20}); @@ -81,7 +82,7 @@ void main() { .substitute("@id:text @foo", {"id": "1';select", "foo": "3\\4"}); // ' 1 ' ' ; s e l e c t ' sp sp E ' 3 \ \ 4 ' - expect(UTF8.encode(result), [ + expect(convert.utf8.encode(result), [ 39, 49, 39, From 3ba7399228e1570c08794ef0fd6e9fe5ce855b95 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Thu, 5 Apr 2018 13:22:02 -0400 Subject: [PATCH 30/73] Adds UUID data type (#36) * Add UUID data type * Pubspec + changelog * Remove whitespace * Add some new tests * Rebase from master --- CHANGELOG.md | 4 + lib/src/binary_codec.dart | 286 +++++++++++++++++++++++--------------- lib/src/query.dart | 4 +- lib/src/substituter.dart | 2 + lib/src/types.dart | 8 +- pubspec.yaml | 2 +- test/decode_test.dart | 19 ++- test/encoding_test.dart | 46 +++++- test/query_test.dart | 30 ++-- 9 files changed, 265 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c84ec..b0e6940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.9 + +- Add full support for `UUID` columns. + ## 0.9.8 - Preserve error stacktrace on various query or transaction errors. diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index abc1afe..ad38270 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -17,140 +17,174 @@ class PostgresBinaryEncoder extends Converter { } switch (dataType) { - case PostgreSQLDataType.boolean: { - if (value is! bool) { - throw new FormatException( - "Invalid type for parameter value. Expected: bool Got: ${value + case PostgreSQLDataType.boolean: + { + if (value is! bool) { + throw new FormatException("Invalid type for parameter value. Expected: bool Got: ${value .runtimeType}"); - } + } - var bd = new ByteData(1); - bd.setUint8(0, value ? 1 : 0); - return bd.buffer.asUint8List(); - } + var bd = new ByteData(1); + bd.setUint8(0, value ? 1 : 0); + return bd.buffer.asUint8List(); + } case PostgreSQLDataType.bigSerial: - case PostgreSQLDataType.bigInteger: { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value + case PostgreSQLDataType.bigInteger: + { + if (value is! int) { + throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value .runtimeType}"); - } + } - var bd = new ByteData(8); - bd.setInt64(0, value); - return bd.buffer.asUint8List(); - } + var bd = new ByteData(8); + bd.setInt64(0, value); + return bd.buffer.asUint8List(); + } case PostgreSQLDataType.serial: - case PostgreSQLDataType.integer: { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value + case PostgreSQLDataType.integer: + { + if (value is! int) { + throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value .runtimeType}"); - } + } - var bd = new ByteData(4); - bd.setInt32(0, value); - return bd.buffer.asUint8List(); - } - case PostgreSQLDataType.smallInteger: { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); + var bd = new ByteData(4); + bd.setInt32(0, value); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(2); - bd.setInt16(0, value); - return bd.buffer.asUint8List(); - } - case PostgreSQLDataType.text: { - if (value is! String) { - throw new FormatException( - "Invalid type for parameter value. Expected: String Got: ${value + case PostgreSQLDataType.smallInteger: + { + if (value is! int) { + throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value .runtimeType}"); + } + + var bd = new ByteData(2); + bd.setInt16(0, value); + return bd.buffer.asUint8List(); } + case PostgreSQLDataType.text: + { + if (value is! String) { + throw new FormatException("Invalid type for parameter value. Expected: String Got: ${value + .runtimeType}"); + } - return UTF8.encode(value); - } - case PostgreSQLDataType.real: { - if (value is! double) { - throw new FormatException( - "Invalid type for parameter value. Expected: double Got: ${value + return UTF8.encode(value); + } + case PostgreSQLDataType.real: + { + if (value is! double) { + throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value .runtimeType}"); + } + + var bd = new ByteData(4); + bd.setFloat32(0, value); + return bd.buffer.asUint8List(); } + case PostgreSQLDataType.double: + { + if (value is! double) { + throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value + .runtimeType}"); + } - var bd = new ByteData(4); - bd.setFloat32(0, value); - return bd.buffer.asUint8List(); - } - case PostgreSQLDataType.double: { - if (value is! double) { - throw new FormatException( - "Invalid type for parameter value. Expected: double Got: ${value + var bd = new ByteData(8); + bd.setFloat64(0, value); + return bd.buffer.asUint8List(); + } + case PostgreSQLDataType.date: + { + if (value is! DateTime) { + throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value .runtimeType}"); + } + + var bd = new ByteData(4); + bd.setInt32(0, value.toUtc().difference(new DateTime.utc(2000)).inDays); + return bd.buffer.asUint8List(); } - var bd = new ByteData(8); - bd.setFloat64(0, value); - return bd.buffer.asUint8List(); - } - case PostgreSQLDataType.date: { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value + case PostgreSQLDataType.timestampWithoutTimezone: + { + if (value is! DateTime) { + throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value .runtimeType}"); - } + } - var bd = new ByteData(4); - bd.setInt32(0, value.toUtc().difference(new DateTime.utc(2000)).inDays); - return bd.buffer.asUint8List(); - } + var bd = new ByteData(8); + var diff = value.toUtc().difference(new DateTime.utc(2000)); + bd.setInt64(0, diff.inMicroseconds); + return bd.buffer.asUint8List(); + } - case PostgreSQLDataType.timestampWithoutTimezone: { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value + case PostgreSQLDataType.timestampWithTimezone: + { + if (value is! DateTime) { + throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value .runtimeType}"); + } + + var bd = new ByteData(8); + bd.setInt64(0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); + return bd.buffer.asUint8List(); } - var bd = new ByteData(8); - var diff = value.toUtc().difference(new DateTime.utc(2000)); - bd.setInt64(0, diff.inMicroseconds); - return bd.buffer.asUint8List(); - } + case PostgreSQLDataType.json: + { + var jsonBytes = UTF8.encode(JSON.encode(value)); + final outBuffer = new Uint8List(jsonBytes.length + 1); + outBuffer[0] = 1; + for (var i = 0; i < jsonBytes.length; i++) { + outBuffer[i + 1] = jsonBytes[i]; + } - case PostgreSQLDataType.timestampWithTimezone: { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); + return outBuffer; } - var bd = new ByteData(8); - bd.setInt64( - 0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); - return bd.buffer.asUint8List(); - } - - case PostgreSQLDataType.json: { - var jsonBytes = UTF8.encode(JSON.encode(value)); - final outBuffer = new Uint8List(jsonBytes.length + 1); - outBuffer[0] = 1; - for (var i = 0; i < jsonBytes.length; i++) { - outBuffer[i + 1] = jsonBytes[i]; + case PostgreSQLDataType.byteArray: + { + if (value is! List) { + throw new FormatException("Invalid type for parameter value. Expected: List Got: ${value + .runtimeType}"); + } + return new Uint8List.fromList(value); } - return outBuffer; - } - - case PostgreSQLDataType.byteArray: { - if (value is! List) { - throw new FormatException( - "Invalid type for parameter value. Expected: List Got: ${value + case PostgreSQLDataType.uuid: + { + if (value is! String) { + throw new FormatException("Invalid type for parameter value. Expected: String Got: ${value .runtimeType}"); + } + + final dashUnit = "-".codeUnits.first; + final hexBytes = (value as String).toLowerCase().codeUnits.where((c) => c != dashUnit).toList(); + if (hexBytes.length != 32) { + throw new FormatException( + "Invalid UUID string. There must be exactly 32 hexadecimal (0-9 and a-f) characters and any number of '-' characters."); + } + + final byteConvert = (int charCode) { + if (charCode >= 48 && charCode <= 57) { + return charCode - 48; + } else if (charCode >= 97 && charCode <= 102) { + return charCode - 87; + } + + throw new FormatException("Invalid UUID string. Contains non-hexadecimal character (0-9 and a-f)."); + }; + + final outBuffer = new Uint8List(16); + for (var i = 0; i < hexBytes.length; i += 2) { + final upperByte = byteConvert(hexBytes[i]); + final lowerByte = byteConvert(hexBytes[i + 1]); + + outBuffer[i ~/ 2] = upperByte * 16 + lowerByte; + } + return outBuffer; } - return new Uint8List.fromList(value); - } } throw new PostgreSQLException("Unsupported datatype"); @@ -191,21 +225,46 @@ class PostgresBinaryDecoder extends Converter { return buffer.getFloat64(0); case PostgreSQLDataType.timestampWithoutTimezone: case PostgreSQLDataType.timestampWithTimezone: - return new DateTime.utc(2000) - .add(new Duration(microseconds: buffer.getInt64(0))); + return new DateTime.utc(2000).add(new Duration(microseconds: buffer.getInt64(0))); case PostgreSQLDataType.date: - return new DateTime.utc(2000) - .add(new Duration(days: buffer.getInt32(0))); + return new DateTime.utc(2000).add(new Duration(days: buffer.getInt32(0))); - case PostgreSQLDataType.json: { - // Removes version which is first character and currently always '1' - final bytes = value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1); - return JSON.decode(UTF8.decode(bytes)); - } + case PostgreSQLDataType.json: + { + // Removes version which is first character and currently always '1' + final bytes = value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1); + return JSON.decode(UTF8.decode(bytes)); + } case PostgreSQLDataType.byteArray: return value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes); + + case PostgreSQLDataType.uuid: + { + final codeDash = "-".codeUnitAt(0); + + final cipher = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + final byteConvert = (int value) { + return cipher[value]; + }; + + final buf = new StringBuffer(); + for (var i = 0; i < buffer.lengthInBytes; i++) { + final byteValue = buffer.getUint8(i); + final upperByteValue = byteValue ~/ 16; + + final upperByteHex = byteConvert(upperByteValue); + final lowerByteHex = byteConvert(byteValue - (upperByteValue * 16)); + buf.write(upperByteHex); + buf.write(lowerByteHex); + if (i == 3 || i == 5 || i == 7 || i == 9) { + buf.writeCharCode(codeDash); + } + } + + return buf.toString(); + } } return value; @@ -223,6 +282,7 @@ class PostgresBinaryDecoder extends Converter { 1082: PostgreSQLDataType.date, 1114: PostgreSQLDataType.timestampWithoutTimezone, 1184: PostgreSQLDataType.timestampWithTimezone, + 2950: PostgreSQLDataType.uuid, 3802: PostgreSQLDataType.json }; -} \ No newline at end of file +} diff --git a/lib/src/query.dart b/lib/src/query.dart index 71400c7..5428fd6 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -252,6 +252,7 @@ class PostgreSQLFormatToken { } class PostgreSQLFormatIdentifier { + static Map typeStringToCodeMap = { "text": PostgreSQLDataType.text, "serial": PostgreSQLDataType.serial, @@ -266,7 +267,8 @@ class PostgreSQLFormatIdentifier { "timestamp": PostgreSQLDataType.timestampWithoutTimezone, "timestamptz": PostgreSQLDataType.timestampWithTimezone, "jsonb": PostgreSQLDataType.json, - "bytea": PostgreSQLDataType.byteArray + "bytea": PostgreSQLDataType.byteArray, + "uuid": PostgreSQLDataType.uuid }; PostgreSQLFormatIdentifier(String t) { diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 33f4123..0763a59 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -43,6 +43,8 @@ class PostgreSQLFormat { return "jsonb"; case PostgreSQLDataType.byteArray: return "bytea"; + case PostgreSQLDataType.uuid: + return "uuid"; } return null; diff --git a/lib/src/types.dart b/lib/src/types.dart index 9f62fcc..caf123b 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -55,5 +55,11 @@ enum PostgreSQLDataType { /// Must be a [List]. /// /// Each element of the list must fit into a byte (0-255). - byteArray + byteArray, + + /// Must be a [String]. + /// + /// Must contain 32 hexadecimal characters. May contain any number of '-' characters. + /// When returned from database, format will be xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. + uuid } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d4f1a95..3661a93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 0.9.8 +version: 0.9.9 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: diff --git a/test/decode_test.dart b/test/decode_test.dart index 9a50741..f38cf50 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -10,23 +10,24 @@ void main() { await connection.execute(""" CREATE TEMPORARY TABLE t ( i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, - t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb, ba bytea) + t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb, ba bytea, + u uuid) """); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba) " + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " "VALUES (-2147483648, -9223372036854775808, TRUE, -32768, " "'string', 10.0, 10.0, '1983-11-06', " "'1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000', " - "'{\"key\":\"value\"}', E'\\\\000')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba) " + "'{\"key\":\"value\"}', E'\\\\000', '00000000-0000-0000-0000-000000000000')"); + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " "VALUES (2147483647, 9223372036854775807, FALSE, 32767, " "'a significantly longer string to the point where i doubt this actually matters', " "10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', " "'2183-11-06 00:00:00.999999', " - "'[{\"key\":1}]', E'\\\\377')"); + "'[{\"key\":1}]', E'\\\\377', 'FFFFFFFF-ffff-ffff-ffff-ffffffffffff')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba) " - "VALUES (null, null, null, null, null, null, null, null, null, null, null, null)"); + await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " + "VALUES (null, null, null, null, null, null, null, null, null, null, null, null, null)"); }); tearDown(() async { await connection?.close(); @@ -56,6 +57,7 @@ void main() { expect(row1[11], equals(new DateTime.utc(1983, 11, 6, 6))); expect(row1[12], equals({"key": "value"})); expect(row1[13], equals([0])); + expect(row1[14], equals("00000000-0000-0000-0000-000000000000")); // upper bound row expect(row2[0], equals(2147483647)); @@ -78,6 +80,8 @@ void main() { {"key": 1} ])); expect(row2[13], equals([255])); + expect(row2[14], equals("ffffffff-ffff-ffff-ffff-ffffffffffff")); + // all null row expect(row3[0], isNull); @@ -94,6 +98,7 @@ void main() { expect(row3[11], isNull); expect(row3[12], isNull); expect(row3[13], isNull); + expect(row3[14], isNull); }); test("Fetch/insert empty string", () async { diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 3b3602b..3abca10 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -170,7 +170,6 @@ void main() { } on FormatException catch (e) { expect(e.toString(), contains("Expected: DateTime")); } - }); test("jsonb", () async { @@ -186,7 +185,7 @@ void main() { try { await conn.query("INSERT INTO t (v) VALUES (@v:jsonb)", substitutionValues: {"v": new DateTime.now()}); fail('unreachable'); - } on JsonUnsupportedObjectError catch (e) {} + } on JsonUnsupportedObjectError catch (_) {} }); test("bytea", () async { @@ -201,6 +200,18 @@ void main() { expect(e.toString(), contains("Expected: List")); } }); + + test("uuid", () async { + await expectInverse("00000000-0000-0000-0000-000000000000", PostgreSQLDataType.uuid); + await expectInverse("12345678-abcd-efab-cdef-012345678901", PostgreSQLDataType.uuid); + + try { + await conn.query("INSERT INTO t (v) VALUES (@v:uuid)", substitutionValues: {"v": new DateTime.now()}); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Expected: String")); + } + }); }); group("Text encoders", () { @@ -322,6 +333,37 @@ void main() { expect(u.hasCachedBytes, true); expect(v.hasCachedBytes, true); }); + + test("Invalid UUID encoding", () { + final converter = new PostgresBinaryEncoder(PostgreSQLDataType.uuid); + try { + converter.convert("z0000000-0000-0000-0000-000000000000"); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Invalid UUID string")); + } + + try { + converter.convert(123123); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Invalid type for parameter")); + } + + try { + converter.convert("0000000-0000-0000-0000-000000000000"); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Invalid UUID string")); + } + + try { + converter.convert("00000000-0000-0000-0000-000000000000f"); + fail('unreachable'); + } on FormatException catch (e) { + expect(e.toString(), contains("Invalid UUID string")); + } + }); } Future expectInverse(dynamic value, PostgreSQLDataType dataType) async { diff --git a/test/query_test.dart b/test/query_test.dart index cfb64cd..4c2e883 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -16,7 +16,7 @@ void main() { "(i int, s serial, bi bigint, " "bs bigserial, bl boolean, si smallint, " "t text, f real, d double precision, " - "dt date, ts timestamp, tsz timestamptz, j jsonb)"); + "dt date, ts timestamp, tsz timestamptz, j jsonb, u uuid)"); await connection.execute( "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); await connection @@ -108,7 +108,7 @@ void main() { test("Query without specifying types", () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) values " + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, u) values " "(${PostgreSQLFormat.id("i")}," "${PostgreSQLFormat.id("bi")}," "${PostgreSQLFormat.id("bl")}," @@ -119,7 +119,9 @@ void main() { "${PostgreSQLFormat.id("dt")}," "${PostgreSQLFormat.id("ts")}," "${PostgreSQLFormat.id("tsz")}," - "${PostgreSQLFormat.id("j")}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j", + "${PostgreSQLFormat.id("j")}," + "${PostgreSQLFormat.id("u")}" + ") returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u", substitutionValues: { "i": 1, "bi": 2, @@ -131,7 +133,8 @@ void main() { "dt": new DateTime.utc(2000), "ts": new DateTime.utc(2000, 2), "tsz": new DateTime.utc(2000, 3), - "j": {"a":"b"} + "j": {"a":"b"}, + "u": "01234567-89ab-cdef-0123-0123456789ab" }); var expectedRow = [ @@ -147,17 +150,18 @@ void main() { new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3), - {"a":"b"} + {"a":"b"}, + "01234567-89ab-cdef-0123-0123456789ab" ]; expect(result, [expectedRow]); result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j from t"); + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); expect(result, [expectedRow]); }); test("Query by specifying all types", () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j) values " + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, u) values " "(${PostgreSQLFormat.id("i", type: PostgreSQLDataType.integer)}," "${PostgreSQLFormat.id("bi", type: PostgreSQLDataType.bigInteger)}," "${PostgreSQLFormat.id("bl", type: PostgreSQLDataType.boolean)}," @@ -168,7 +172,9 @@ void main() { "${PostgreSQLFormat.id("dt", type: PostgreSQLDataType.date)}," "${PostgreSQLFormat.id("ts", type: PostgreSQLDataType.timestampWithoutTimezone)}," "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}," - "${PostgreSQLFormat.id("j", type: PostgreSQLDataType.json)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j", + "${PostgreSQLFormat.id("j", type: PostgreSQLDataType.json)}," + "${PostgreSQLFormat.id("u", type: PostgreSQLDataType.uuid)})" + " returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u", substitutionValues: { "i": 1, "bi": 2, @@ -180,7 +186,8 @@ void main() { "dt": new DateTime.utc(2000), "ts": new DateTime.utc(2000, 2), "tsz": new DateTime.utc(2000, 3), - "j": {"key": "value"} + "j": {"key": "value"}, + "u": "01234567-89ab-cdef-0123-0123456789ab" }); var expectedRow = [ @@ -196,12 +203,13 @@ void main() { new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3), - {"key": "value"} + {"key": "value"}, + "01234567-89ab-cdef-0123-0123456789ab" ]; expect(result, [expectedRow]); result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j from t"); + .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); expect(result, [expectedRow]); }); From 8427afaaf6ed45c4e57d69f96bbd5b99b3bb781b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 5 Apr 2018 20:44:29 +0200 Subject: [PATCH 31/73] Handle JSONB operator without throwing an error. (#47) --- lib/src/substituter.dart | 2 ++ test/interpolation_test.dart | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 0763a59..0249c91 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -109,6 +109,8 @@ class PostgreSQLFormat { return items.map((t) { if (t.type == PostgreSQLFormatTokenType.text) { return t.buffer; + } else if (t.buffer.length == 1 && t.buffer.toString() == '@') { + return t.buffer; } else { var identifier = new PostgreSQLFormatIdentifier(t.buffer.toString()); diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 84f2d49..757df26 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -106,4 +106,11 @@ void main() { 39 ]); }); + + test("JSONB operator does not throw", () { + final query = "SELECT id FROM table WHERE data @> '{\"key\": \"value\"}'"; + final results = PostgreSQLFormat.substitute(query, {}); + + expect(results, query); + }); } From 8d38cd6f711c8bfb3c3d12c0d068ee69da266e65 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 6 Apr 2018 10:57:39 -0400 Subject: [PATCH 32/73] A few cleanup items (#46) * Export execution context, update API refs * Adds name column type. Allows bigserial/serial to have their own type name format string * Fixing tests * attempt to return unknown types by utf8 decoding them --- lib/postgres.dart | 1 + lib/src/binary_codec.dart | 16 ++++++++++++++-- lib/src/connection.dart | 4 ++-- lib/src/execution_context.dart | 10 +++++++--- lib/src/query.dart | 3 +-- lib/src/substituter.dart | 2 ++ lib/src/types.dart | 7 ++++++- test/decode_test.dart | 2 -- test/encoding_test.dart | 4 ++-- test/interpolation_test.dart | 20 +++++++++++++++++++- 10 files changed, 54 insertions(+), 15 deletions(-) diff --git a/lib/postgres.dart b/lib/postgres.dart index 148a572..050dc89 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -3,3 +3,4 @@ library postgres; export 'src/connection.dart'; export 'src/types.dart'; export 'src/substituter.dart'; +export 'src/execution_context.dart'; diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index ad38270..1c153dd 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:dart2_constant/convert.dart' as _convert; import 'package:postgres/postgres.dart'; import 'package:postgres/src/types.dart'; @@ -63,6 +64,7 @@ class PostgresBinaryEncoder extends Converter { bd.setInt16(0, value); return bd.buffer.asUint8List(); } + case PostgreSQLDataType.name: case PostgreSQLDataType.text: { if (value is! String) { @@ -207,6 +209,7 @@ class PostgresBinaryDecoder extends Converter { final buffer = new ByteData.view(value.buffer, value.offsetInBytes, value.lengthInBytes); switch (dataType) { + case PostgreSQLDataType.name: case PostgreSQLDataType.text: return UTF8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); case PostgreSQLDataType.boolean: @@ -267,12 +270,21 @@ class PostgresBinaryDecoder extends Converter { } } - return value; + // We'll try and decode this as a utf8 string and return that + // for many internal types, this is valid. If it fails, + // we just return the bytes and let the caller figure out what to + // do with it. + try { + return _convert.utf8.decode(value); + } catch (_) { + return value; + } } static final Map typeMap = { 16: PostgreSQLDataType.boolean, 17: PostgreSQLDataType.byteArray, + 19: PostgreSQLDataType.name, 20: PostgreSQLDataType.bigInteger, 21: PostgreSQLDataType.smallInteger, 23: PostgreSQLDataType.integer, @@ -283,6 +295,6 @@ class PostgresBinaryDecoder extends Converter { 1114: PostgreSQLDataType.timestampWithoutTimezone, 1184: PostgreSQLDataType.timestampWithTimezone, 2950: PostgreSQLDataType.uuid, - 3802: PostgreSQLDataType.json + 3802: PostgreSQLDataType.json, }; } diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 14b5f4d..d5c141b 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -81,7 +81,7 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// Whether or not this connection is open or not. /// - /// This is [true] when this instance is first created and after it has been closed or encountered an unrecoverable error. + /// This is `true` when this instance is first created and after it has been closed or encountered an unrecoverable error. /// If a connection has already been opened and this value is now true, the connection cannot be reopened and a new instance /// must be created. bool get isClosed => _connectionState is _PostgreSQLConnectionStateClosed; @@ -408,7 +408,7 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo orderedTableNames.forEach((tableName) { iterator.moveNext(); if (tableName.first != null) { - _tableOIDNameMap[iterator.current] = convert.utf8.decode(tableName.first); + _tableOIDNameMap[iterator.current] = tableName.first; } }); } diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 4f7216f..7c60dc9 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -1,4 +1,8 @@ import 'dart:async'; +import 'query.dart'; +import 'types.dart'; +import 'substituter.dart'; +import 'connection.dart'; abstract class PostgreSQLExecutionContext { /// Executes a query on this context. @@ -9,12 +13,12 @@ abstract class PostgreSQLExecutionContext { /// /// connection.query("SELECT * FROM table WHERE id = @idParam", {"idParam" : 2}); /// - /// The type of the value is inferred by default, but can be made more specific by adding ':type" to the parameter pattern in the format string. The possible values - /// are declared as static variables in [PostgreSQLCodec] (e.g., [PostgreSQLCodec.TypeInt4]). For example: + /// The type of the value is inferred by default, but should be made more specific by adding ':type" to the parameter pattern in the format string. For example: /// /// connection.query("SELECT * FROM table WHERE id = @idParam:int4", {"idParam" : 2}); /// - /// You may also use [PostgreSQLFormat.id] to create parameter patterns. + /// Available types are listed in [PostgreSQLFormatIdentifier.typeStringToCodeMap]. Some types have multiple options. It is preferable to use the [PostgreSQLFormat.id] + /// function to add parameters to a query string. This method inserts a parameter name and the appropriate ':type' string for a [PostgreSQLDataType]. /// /// If successful, the returned [Future] completes with a [List] of rows. Each is row is represented by a [List] of column values for that row that were returned by the query. /// diff --git a/lib/src/query.dart b/lib/src/query.dart index 5428fd6..b0659fa 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -255,8 +255,6 @@ class PostgreSQLFormatIdentifier { static Map typeStringToCodeMap = { "text": PostgreSQLDataType.text, - "serial": PostgreSQLDataType.serial, - "bigserial": PostgreSQLDataType.bigSerial, "int2": PostgreSQLDataType.smallInteger, "int4": PostgreSQLDataType.integer, "int8": PostgreSQLDataType.bigInteger, @@ -268,6 +266,7 @@ class PostgreSQLFormatIdentifier { "timestamptz": PostgreSQLDataType.timestampWithTimezone, "jsonb": PostgreSQLDataType.json, "bytea": PostgreSQLDataType.byteArray, + "name": PostgreSQLDataType.name, "uuid": PostgreSQLDataType.uuid }; diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 0249c91..48b34aa 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -43,6 +43,8 @@ class PostgreSQLFormat { return "jsonb"; case PostgreSQLDataType.byteArray: return "bytea"; + case PostgreSQLDataType.name: + return "name"; case PostgreSQLDataType.uuid: return "uuid"; } diff --git a/lib/src/types.dart b/lib/src/types.dart index caf123b..11add54 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -52,11 +52,16 @@ enum PostgreSQLDataType { /// Values will be encoded via [JSON.encode] before being sent to the database. json, - /// Must be a [List]. + /// Must be a [List] of [int]. /// /// Each element of the list must fit into a byte (0-255). byteArray, + /// Must be a [String] + /// + /// Used for internal pg structure names + name, + /// Must be a [String]. /// /// Must contain 32 hexadecimal characters. May contain any number of '-' characters. diff --git a/test/decode_test.dart b/test/decode_test.dart index f38cf50..2da41c1 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -128,6 +128,4 @@ void main() { [null] ]); }); - - test("Timezone concerns", () {}); } diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 3abca10..407471b 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -68,7 +68,7 @@ void main() { await expectInverse(0, PostgreSQLDataType.serial); await expectInverse(1, PostgreSQLDataType.serial); try { - await conn.query("INSERT INTO t (v) VALUES (@v:serial)", substitutionValues: {"v": "not-serial"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int4)", substitutionValues: {"v": "not-serial"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); @@ -93,7 +93,7 @@ void main() { await expectInverse(0, PostgreSQLDataType.bigSerial); await expectInverse(1, PostgreSQLDataType.bigSerial); try { - await conn.query("INSERT INTO t (v) VALUES (@v:bigserial)", substitutionValues: {"v": "not-bigserial"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int8)", substitutionValues: {"v": "not-bigserial"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 757df26..9082802 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -1,9 +1,27 @@ import 'package:dart2_constant/convert.dart' as convert; + +import 'package:postgres/postgres.dart'; +import 'package:postgres/src/query.dart'; import 'package:test/test.dart'; -import 'package:postgres/src/substituter.dart'; void main() { + test("Ensure all types/format type mappings are available and accurate", () { + PostgreSQLDataType.values.where((t) => t != PostgreSQLDataType.bigSerial && t != PostgreSQLDataType.serial).forEach((t) { + expect(PostgreSQLFormatIdentifier.typeStringToCodeMap.values.contains(t), true); + final code = PostgreSQLFormat.dataTypeStringForDataType(t); + expect(PostgreSQLFormatIdentifier.typeStringToCodeMap[code], t); + }); + }); + + test("Ensure bigserial gets translated to int8", () { + expect(PostgreSQLFormat.dataTypeStringForDataType(PostgreSQLDataType.serial), "int4"); + }); + + test("Ensure serial gets translated to int4", () { + expect(PostgreSQLFormat.dataTypeStringForDataType(PostgreSQLDataType.bigSerial), "int8"); + }); + test("Simple replacement", () { var result = PostgreSQLFormat.substitute("@id", {"id": 20}); expect(result, equals("20")); From eebfc7387d3e2af088efb01fb7248cb9e6ca1fe8 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Fri, 25 May 2018 14:33:06 -0400 Subject: [PATCH 33/73] Fix issues with --preview-dart-2 flag. (#52) * Fix issues with --preview-dart-2 flag. Improve transaction error handling * remove unused var * let travis grab dev channel * Fix issue with transactions --- .travis.yml | 2 + CHANGELOG.md | 4 + ci/script.sh | 6 ++ lib/src/binary_codec.dart | 11 ++- lib/src/connection.dart | 138 ++++++++++++++------------------ lib/src/connection_fsm.dart | 22 +---- lib/src/query.dart | 25 +++--- lib/src/query_queue.dart | 25 ++++-- lib/src/server_messages.dart | 15 ++-- lib/src/text_codec.dart | 4 +- lib/src/transaction_proxy.dart | 75 ++++++++++++----- lib/src/types.dart | 6 +- lib/src/utf8_backed_string.dart | 6 +- pubspec.yaml | 5 +- test/connection_test.dart | 64 ++++++++------- test/encoding_test.dart | 44 +++++----- test/interpolation_test.dart | 4 +- test/transaction_test.dart | 74 +++++++++++++++-- 18 files changed, 313 insertions(+), 217 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd4388d..3f04b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: dart sudo: required +dart: + - dev addons: postgresql: "9.5" services: diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e6940..dc4890b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.0 + +- Adds support for Dart 2 + ## 0.9.9 - Add full support for `UUID` columns. diff --git a/ci/script.sh b/ci/script.sh index 6252f29..5977cc9 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -1,6 +1,12 @@ #!/bin/bash set -e + +export DART_VM_OPTIONS=--preview-dart-2 + pub run test -j 1 -r expanded + +export DART_VM_OPTIONS="" + if [[ "$TRAVIS_BRANCH" == "master" ]]; then pub global activate -sgit https://github.com/stablekernel/codecov_dart.git dart_codecov_generator --report-on=lib/ --verbose --no-html diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index 1c153dd..cc4c2eb 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:dart2_constant/convert.dart' as _convert; import 'package:postgres/postgres.dart'; import 'package:postgres/src/types.dart'; @@ -72,7 +71,7 @@ class PostgresBinaryEncoder extends Converter { .runtimeType}"); } - return UTF8.encode(value); + return utf8.encode(value); } case PostgreSQLDataType.real: { @@ -135,7 +134,7 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.json: { - var jsonBytes = UTF8.encode(JSON.encode(value)); + var jsonBytes = utf8.encode(json.encode(value)); final outBuffer = new Uint8List(jsonBytes.length + 1); outBuffer[0] = 1; for (var i = 0; i < jsonBytes.length; i++) { @@ -211,7 +210,7 @@ class PostgresBinaryDecoder extends Converter { switch (dataType) { case PostgreSQLDataType.name: case PostgreSQLDataType.text: - return UTF8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); + return utf8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); case PostgreSQLDataType.boolean: return buffer.getInt8(0) != 0; case PostgreSQLDataType.smallInteger: @@ -237,7 +236,7 @@ class PostgresBinaryDecoder extends Converter { { // Removes version which is first character and currently always '1' final bytes = value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1); - return JSON.decode(UTF8.decode(bytes)); + return json.decode(utf8.decode(bytes)); } case PostgreSQLDataType.byteArray: @@ -275,7 +274,7 @@ class PostgresBinaryDecoder extends Converter { // we just return the bytes and let the caller figure out what to // do with it. try { - return _convert.utf8.decode(value); + return utf8.decode(value); } catch (_) { return value; } diff --git a/lib/src/connection.dart b/lib/src/connection.dart index d5c141b..19f84bc 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -4,8 +4,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import 'package:dart2_constant/convert.dart' as convert; - import 'package:postgres/src/query_cache.dart'; import 'package:postgres/src/execution_context.dart'; import 'package:postgres/src/query_queue.dart'; @@ -95,7 +93,6 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin QueryCache _cache = new QueryCache(); Socket _socket; MessageFramer _framer = new MessageFramer(); - int _secretKey; List _salt; @@ -112,41 +109,43 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// on this connection afterwards. If the connection fails to be established for any reason - including authentication - /// the returned [Future] will return with an error. /// - /// 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. + /// 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 { if (_hasConnectedPreviously) { throw new PostgreSQLException("Attempting to reopen a closed connection. Create a new instance instead."); } - _hasConnectedPreviously = true; - _socket = await Socket.connect(host, port).timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); + try { + _hasConnectedPreviously = true; + _socket = await Socket.connect(host, port).timeout(new Duration(seconds: timeoutInSeconds)); - _framer = new MessageFramer(); - if (useSSL) { - _socket = await _upgradeSocketToSSL(_socket, timeout: timeoutInSeconds); - } + _framer = new MessageFramer(); + if (useSSL) { + _socket = await _upgradeSocketToSSL(_socket, timeout: timeoutInSeconds); + } - var connectionComplete = new Completer(); + var connectionComplete = new Completer(); + _socket.listen(_readData, onError: (err, st) => _close(err, st), onDone: () => _close()); - _socket.listen(_readData, onError: _handleSocketError, onDone: _handleSocketClosed); + _transitionToState(new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); - _transitionToState(new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); + await connectionComplete.future.timeout(new Duration(seconds: timeoutInSeconds)); + } on TimeoutException catch (e, st) { + final err = new PostgreSQLException("Failed to connect to database $host:$port/$databaseName failed to connect."); + await _close(err, st); + rethrow; + } catch (e, st) { + await _close(e, st); - return connectionComplete.future.timeout(new Duration(seconds: timeoutInSeconds), onTimeout: _timeout); + rethrow; + } } /// Closes a connection. /// /// After the returned [Future] completes, this connection can no longer be used to execute queries. Any queries in progress or queued are cancelled. - Future close() async { - _connectionState = new _PostgreSQLConnectionStateClosed(); - - await _socket?.close(); - - _queue.cancel(); - - return _cleanup(); - } + Future close() => _close(); /// Executes a series of queries inside a transaction on this connection. /// @@ -157,10 +156,7 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// the transaction will fail and previous statements within the transaction will not be committed. The [Future] /// returned from this method will be completed with the error from the first failing query. /// - /// Do not catch exceptions within a transaction block, as it will prevent the transaction exception handler from fulfilling a - /// transaction. - /// - /// Transactions may be cancelled by issuing [PostgreSQLExecutionContext.cancelTransaction] + /// Transactions may be cancelled by invoking [PostgreSQLExecutionContext.cancelTransaction] /// within the transaction. This will cause this method to return a [Future] with a value of [PostgreSQLRollback]. This method does not throw an exception /// if the transaction is cancelled in this way. /// @@ -193,15 +189,6 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin //////// - void _timeout() { - _connectionState = new _PostgreSQLConnectionStateClosed(); - _socket?.destroy(); - - _queue.cancel(); - _cleanup(); - throw new PostgreSQLException("Timed out trying to connect to database postgres://$host:$port/$databaseName."); - } - void _transitionToState(_PostgreSQLConnectionState newState) { if (identical(newState, _connectionState)) { return; @@ -216,6 +203,15 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin _connectionState.connection = this; } + Future _close([dynamic error, StackTrace trace]) async { + _connectionState = new _PostgreSQLConnectionStateClosed(); + + await _socket?.close(); + await _notifications?.close(); + + _queue?.cancel(error, trace); + } + void _readData(List bytes) { // Note that the way this method works, if a query is in-flight, and we move to the closed state // manually, the delivery of the bytes from the socket is sent to the 'Closed State', @@ -234,27 +230,12 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin _transitionToState(_connectionState.onMessage(msg)); } } catch (e, st) { - _handleSocketError(e, st); + _close(e, st); } } } - void _handleSocketError(Object error, StackTrace stack) { - _connectionState = new _PostgreSQLConnectionStateClosed(); - _socket.destroy(); - - _queue.cancel(error, stack); - _cleanup(); - } - - void _handleSocketClosed() { - _connectionState = new _PostgreSQLConnectionStateClosed(); - - _queue.cancel(); - _cleanup(); - } - - Future _upgradeSocketToSSL(Socket originalSocket, {int timeout: 30}) async { + Future _upgradeSocketToSSL(Socket originalSocket, {int timeout: 30}) { var sslCompleter = new Completer(); originalSocket.listen( @@ -278,18 +259,15 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin byteBuffer.setUint32(4, 80877103); originalSocket.add(byteBuffer.buffer.asUint8List()); - var responseByte = await sslCompleter.future.timeout(new Duration(seconds: timeout), onTimeout: _timeout); - if (responseByte == 83) { + return sslCompleter.future.timeout(new Duration(seconds: timeout)).then((responseByte) { + if (responseByte != 83) { + throw new PostgreSQLException("The database server is not accepting SSL connections."); + } + return SecureSocket .secure(originalSocket, onBadCertificate: (certificate) => true) - .timeout(new Duration(seconds: timeout), onTimeout: _timeout); - } - - throw new PostgreSQLException("SSL not allowed for this connection."); - } - - Future _cleanup() async { - await _notifications.close(); + .timeout(new Duration(seconds: timeout)); + }); } } @@ -372,7 +350,7 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName - final tableOIDs = new Set.from(columns.map((f) => f.tableID)); + final tableOIDs = new Set.from(columns.map((f) => f.tableID)); final List unresolvedTableOIDs = tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); @@ -387,7 +365,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); return rows.map((row) { - var rowMap = new Map.fromIterable(tableNames, key: (name) => name, value: (_) => {}); + var rowMap = new Map>.fromIterable(tableNames, + key: (name) => name, value: (_) => {}); final iterator = columns.iterator; row.forEach((column) { @@ -414,18 +393,25 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo } Future _enqueue(Query query, {int timeoutInSeconds: 30}) async { - _queue.add(query); - _connection._transitionToState(_connection._connectionState.awake()); + if (_queue.add(query)) { + _connection._transitionToState(_connection._connectionState.awake()); - try { - final result = await query.future.timeout(new Duration(seconds: timeoutInSeconds)); - _connection._cache.add(query); - _queue.remove(query); - return result; - } catch (e, st) { - _queue.remove(query); - await _onQueryError(query, e, st); - rethrow; + try { + final result = await query.future.timeout(new Duration(seconds: timeoutInSeconds)); + _connection._cache.add(query); + _queue.remove(query); + return result; + } catch (e, st) { + _queue.remove(query); + await _onQueryError(query, e, st); + rethrow; + } + } else { + // wrap the synchronous future in an async future to ensure that + // the caller behaves correctly in this condition. otherwise, + // the caller would complete synchronously. This future + // will always complete as a cancellation error. + return new Future(() async => query.future); } } diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 669979d..f94e671 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -232,13 +232,12 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { // We ignore NoData, as it doesn't tell us anything we don't already know // or care about. - // print("(${query.statement}) -> $message"); + // print("(${query.statement}) -> $message"); if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateTransactionError) { - // This should cancel the transaction, we may have to send a commit here query.completeError(returningException); - return new _PostgreSQLConnectionStateTransactionFailure(query.transaction); + return new _PostgreSQLConnectionStateReadyInTransaction(query.transaction); } if (returningException != null) { @@ -304,28 +303,13 @@ class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnection } catch (e, st) { scheduleMicrotask(() { q.completeError(e, st); - connection._transitionToState(new _PostgreSQLConnectionStateTransactionFailure(transaction)); }); - return new _PostgreSQLConnectionStateDeferredFailure(); + return this; } } } -/* - Transaction error state - */ - -class _PostgreSQLConnectionStateTransactionFailure extends _PostgreSQLConnectionState { - _PostgreSQLConnectionStateTransactionFailure(this.transaction); - - _TransactionProxy transaction; - - _PostgreSQLConnectionState awake() { - return new _PostgreSQLConnectionStateReadyInTransaction(transaction); - } -} - /* Hack for deferred error */ diff --git a/lib/src/query.dart b/lib/src/query.dart index b0659fa..1d045f6 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -3,8 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:dart2_constant/convert.dart' as convert; - import 'package:postgres/src/binary_codec.dart'; import 'package:postgres/src/execution_context.dart'; @@ -20,7 +18,6 @@ class Query { bool onlyReturnAffectedRowCount = false; String statementIdentifier; - Completer _onComplete = new Completer.sync(); Future get future => _onComplete.future; @@ -30,7 +27,11 @@ class Query { final PostgreSQLConnection connection; List specifiedParameterTypeCodes; + List> rows = []; + + CachedQuery cache; + Completer _onComplete = new Completer.sync(); List _fieldDescriptions; List get fieldDescriptions => _fieldDescriptions; @@ -40,10 +41,6 @@ class Query { cache?.fieldDescriptions = fds; } - List rows = []; - - CachedQuery cache; - void sendSimple(Socket socket) { var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues); var queryMessage = new QueryMessage(sqlString); @@ -135,15 +132,23 @@ class Query { } void complete(int rowsAffected) { + if (_onComplete.isCompleted) { + return; + } + if (onlyReturnAffectedRowCount) { - _onComplete.complete(rowsAffected); + _onComplete.complete(rowsAffected as T); return; } - _onComplete.complete(rows); + _onComplete.complete(rows as T); } void completeError(dynamic error, [StackTrace stackTrace]) { + if (_onComplete.isCompleted) { + return; + } + _onComplete.completeError(error, stackTrace); } @@ -180,7 +185,7 @@ class ParameterValue { ParameterValue.text(dynamic value) : isBinary = false { if (value != null) { final converter = new PostgresTextEncoder(false); - bytes = convert.utf8.encode(converter.convert(value)); + bytes = utf8.encode(converter.convert(value)); } length = bytes?.length; } diff --git a/lib/src/query_queue.dart b/lib/src/query_queue.dart index 4356517..2135f17 100644 --- a/lib/src/query_queue.dart +++ b/lib/src/query_queue.dart @@ -6,6 +6,9 @@ import 'package:postgres/src/query.dart'; class QueryQueue extends ListBase> implements List> { List> _inner = []; + bool _isCancelled = false; + + PostgreSQLException get _cancellationException => new PostgreSQLException("Query cancelled due to the database connection closing."); Query get pending { if (_inner.isEmpty) { @@ -14,8 +17,9 @@ class QueryQueue extends ListBase> implements List return _inner.first; } - void cancel([Object error, StackTrace stackTrace]) { - error ??= "Cancelled"; + void cancel([dynamic error, StackTrace stackTrace]) { + _isCancelled = true; + error ??= _cancellationException; final existing = _inner; _inner = []; @@ -23,10 +27,8 @@ class QueryQueue extends ListBase> implements List // get the error and not the close message, since completeError is // synchronous. scheduleMicrotask(() { - var exception = - new PostgreSQLException("Connection closed or query cancelled (reason: $error).", stackTrace: stackTrace); existing?.forEach((q) { - q.completeError(exception, stackTrace); + q.completeError(error, stackTrace); }); }); } @@ -45,9 +47,20 @@ class QueryQueue extends ListBase> implements List @override void operator []=(int index, Query value) => _inner[index] = value; + void addEvenIfCancelled(Query element) { + _inner.add(element); + } + @override - void add(Query element) { + bool add(Query element) { + if (_isCancelled) { + element.future.catchError((_) {}); + element.completeError(_cancellationException); + return false; + } + _inner.add(element); + return true; } @override diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index 3d777ca..b6e5668 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -1,7 +1,6 @@ +import 'dart:convert'; import 'dart:typed_data'; -import 'package:dart2_constant/convert.dart' as convert; - import 'connection.dart'; import 'query.dart'; @@ -58,8 +57,8 @@ class ParameterStatusMessage extends ServerMessage { String value; void readBytes(Uint8List bytes) { - name = convert.utf8.decode(bytes.sublist(0, bytes.indexOf(0))); - value = convert.utf8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + name = utf8.decode(bytes.sublist(0, bytes.indexOf(0))); + value = utf8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } } @@ -71,7 +70,7 @@ class ReadyForQueryMessage extends ServerMessage { String state; void readBytes(Uint8List bytes) { - state = convert.utf8.decode(bytes); + state = utf8.decode(bytes); } } @@ -140,8 +139,8 @@ class NotificationResponseMessage extends ServerMessage { void readBytes(Uint8List bytes) { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); processID = view.getUint32(0); - channel = convert.utf8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); - payload = convert.utf8.decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); + channel = utf8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); + payload = utf8.decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); } } @@ -151,7 +150,7 @@ class CommandCompleteMessage extends ServerMessage { static RegExp identifierExpression = new RegExp(r"[A-Z ]*"); void readBytes(Uint8List bytes) { - var str = convert.utf8.decode(bytes.sublist(0, bytes.length - 1)); + var str = utf8.decode(bytes.sublist(0, bytes.length - 1)); var match = identifierExpression.firstMatch(str); if (match.end < str.length) { diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index 78a29e0..844c15e 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -152,9 +152,9 @@ class PostgresTextEncoder extends Converter { } if (value is String) { - return "'${JSON.encode(value)}'"; + return "'${json.encode(value)}'"; } - return "${JSON.encode(value)}"; + return "${json.encode(value)}"; } } diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index c46fa8d..69c9b6a 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -6,7 +6,11 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp _TransactionProxy(this._connection, this.executionBlock) { beginQuery = new Query("BEGIN", {}, _connection, this)..onlyReturnAffectedRowCount = true; - beginQuery.future.then(startTransaction).catchError(_onBeginFailure); + beginQuery.future.then(startTransaction).catchError((err, st) { + new Future(() { + completer.completeError(err, st); + }); + }); } Query beginQuery; @@ -15,14 +19,12 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp Future get future => completer.future; final PostgreSQLConnection _connection; + PostgreSQLExecutionContext get _transaction => this; _TransactionQuerySignature executionBlock; bool _hasFailed = false; - - Future commit() async { - await execute("COMMIT"); - } + bool _hasRolledBack = false; void cancelTransaction({String reason: null}) { throw new _TransactionRollbackException(reason); @@ -37,12 +39,12 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp // in the executionBlock are given a chance to run await new Future(() => null); } on _TransactionRollbackException catch (rollback) { - _queue.clear(); - await execute("ROLLBACK"); - completer.complete(new PostgreSQLRollback._(rollback.reason)); + await _cancelAndRollback(rollback); + return; } catch (e, st) { await _transactionFailed(e, st); + return; } @@ -53,27 +55,60 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp await _queue.last.future.catchError((_) {}); } - await execute("COMMIT"); - - completer.complete(result); + if (!_hasRolledBack && !_hasFailed) { + await execute("COMMIT"); + completer.complete(result); + } } - Future _onBeginFailure(dynamic err) async { - completer.completeError(err); + Future _cancelAndRollback(dynamic object, [StackTrace trace]) async { + if (_hasRolledBack) { + return; + } + + _hasRolledBack = true; + // We'll wrap each query in an error handler here to make sure the query cancellation error + // is only emitted from the transaction itself. + _queue.forEach((q) { + q.future.catchError((_) {}); + }); + + final err = new PostgreSQLException("Query failed prior to execution. " + "This query's transaction encountered an error earlier in the transaction " + "that prevented this query from executing."); + _queue.cancel(err); + + var rollback = new Query("ROLLBACK", {}, _connection, _transaction)..onlyReturnAffectedRowCount = true; + _queue.addEvenIfCancelled(rollback); + + _connection._transitionToState(_connection._connectionState.awake()); + + try { + await rollback.future.timeout(new Duration(seconds: 30)); + } finally { + _queue.remove(rollback); + } + + if (object is _TransactionRollbackException) { + completer.complete(new PostgreSQLRollback._(object.reason)); + } else { + completer.completeError(object, trace); + } } Future _transactionFailed(dynamic error, [StackTrace trace]) async { - if (!_hasFailed) { - _hasFailed = true; - _queue.clear(); - await execute("ROLLBACK"); - completer.completeError(error, trace); + if (_hasFailed) { + return; } + + _hasFailed = true; + + await _cancelAndRollback(error, trace); } @override - Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async { - await _transactionFailed(error, trace); + Future _onQueryError(Query query, dynamic error, [StackTrace trace]) { + return _transactionFailed(error, trace); } } diff --git a/lib/src/types.dart b/lib/src/types.dart index 11add54..44238af 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - /* Adding a new type: @@ -47,9 +45,9 @@ enum PostgreSQLDataType { /// Must be a [DateTime] (contains year, month and day only) date, - /// Must be encodable via [JSON.encode]. + /// Must be encodable via [json.encode]. /// - /// Values will be encoded via [JSON.encode] before being sent to the database. + /// Values will be encoded via [json.encode] before being sent to the database. json, /// Must be a [List] of [int]. diff --git a/lib/src/utf8_backed_string.dart b/lib/src/utf8_backed_string.dart index 6d9da61..08bac39 100644 --- a/lib/src/utf8_backed_string.dart +++ b/lib/src/utf8_backed_string.dart @@ -1,4 +1,4 @@ -import 'package:dart2_constant/convert.dart' as convert; +import 'dart:convert'; class UTF8BackedString { UTF8BackedString(this.string); @@ -11,14 +11,14 @@ class UTF8BackedString { int get utf8Length { if (_cachedUTF8Bytes == null) { - _cachedUTF8Bytes = convert.utf8.encode(string); + _cachedUTF8Bytes = utf8.encode(string); } return _cachedUTF8Bytes.length; } List get utf8Bytes { if (_cachedUTF8Bytes == null) { - _cachedUTF8Bytes = convert.utf8.encode(string); + _cachedUTF8Bytes = utf8.encode(string); } return _cachedUTF8Bytes; } diff --git a/pubspec.yaml b/pubspec.yaml index 3661a93..9206792 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,15 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 0.9.9 +version: 1.0.0-beta.1 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: environment: - sdk: '>=1.19.0 <2.0.0' + sdk: ">=2.0.0-dev <3.0.0" dependencies: crypto: ^2.0.0 - dart2_constant: ^1.0.0 dev_dependencies: test: '>=0.12.0 <0.13.0' diff --git a/test/connection_test.dart b/test/connection_test.dart index ebce7e1..e662902 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -91,18 +91,22 @@ void main() { await conn.open(); var errors = []; + final catcher = (e) { + errors.add(e); + return null; + }; var futures = [ - conn.query("select 1", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 2", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 3", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 4", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 5", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 1", allowReuse: false).catchError(catcher), + conn.query("select 2", allowReuse: false).catchError(catcher), + conn.query("select 3", allowReuse: false).catchError(catcher), + conn.query("select 4", allowReuse: false).catchError(catcher), + conn.query("select 5", allowReuse: false).catchError(catcher), ]; await conn.close(); await Future.wait(futures); expect(errors.length, 5); - expect(errors.map((e) => e.message), everyElement(contains("Connection closed"))); + expect(errors.map((e) => e.message), everyElement(contains("Query cancelled"))); }); test( @@ -112,18 +116,22 @@ void main() { await conn.open(); var errors = []; + final catcher = (e) { + errors.add(e); + return null; + }; var futures = [ - conn.query("select 1", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 2", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 3", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 4", allowReuse: false).catchError((e) => errors.add(e)), - conn.query("select 5", allowReuse: false).catchError((e) => errors.add(e)), + conn.query("select 1", allowReuse: false).catchError(catcher), + conn.query("select 2", allowReuse: false).catchError(catcher), + conn.query("select 3", allowReuse: false).catchError(catcher), + conn.query("select 4", allowReuse: false).catchError(catcher), + conn.query("select 5", allowReuse: false).catchError(catcher), ]; await conn.close(); await Future.wait(futures); expect(errors.length, 5); - expect(errors.map((e) => e.message), everyElement(contains("Connection closed"))); + expect(errors.map((e) => e.message), everyElement(contains("Query cancelled"))); }); }); @@ -200,14 +208,16 @@ void main() { group("Unintended user-error situations", () { PostgreSQLConnection conn = null; + Future openFuture; tearDown(() async { + await openFuture; await conn?.close(); }); test("Sending queries to opening connection triggers error", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); - conn.open(); + openFuture = conn.open(); try { await conn.execute("select 1"); @@ -219,7 +229,7 @@ void main() { test("SSL Sending queries to opening connection triggers error", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); - conn.open(); + openFuture = conn.open(); try { await conn.execute("select 1"); @@ -231,7 +241,7 @@ void main() { test("Starting transaction while opening connection triggers error", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); - conn.open(); + openFuture = conn.open(); try { await conn.transaction((ctx) async { @@ -245,7 +255,7 @@ void main() { test("SSL Starting transaction while opening connection triggers error", () async { conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); - conn.open(); + openFuture = conn.open(); try { await conn.transaction((ctx) async { @@ -435,7 +445,7 @@ void main() { }); test("Connection that times out throws appropriate error and cannot be reused", () async { - serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -446,15 +456,14 @@ void main() { try { await conn.open(); - } on PostgreSQLException catch (e) { - expect(e.message, contains("Timed out trying to connect")); - } + fail('unreachable'); + } on TimeoutException {} await expectConnectionIsInvalid(conn); }); test("SSL Connection that times out throws appropriate error and cannot be reused", () async { - serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -465,16 +474,15 @@ void main() { try { await conn.open(); - } on PostgreSQLException catch (e) { - expect(e.message, contains("Timed out trying to connect")); - } + fail('unreachable'); + } on TimeoutException {} await expectConnectionIsInvalid(conn); }); test("Connection that times out triggers future for pending queries", () async { var openCompleter = new Completer(); - serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -493,13 +501,13 @@ void main() { await conn.execute("select 1"); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("closed or query cancelled")); + expect(e.message, contains("Failed to connect")); } }); test("SSL Connection that times out triggers future for pending queries", () async { var openCompleter = new Completer(); - serverSocket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 5433); + serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -510,7 +518,7 @@ void main() { }); var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2, useSSL: true); - conn.open().catchError((e) {}); + conn.open().catchError((e) { return null;}); await openCompleter.future; diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 407471b..03367cf 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:dart2_constant/convert.dart' as convert; -import 'package:dart2_constant/core.dart' as core; import 'package:test/test.dart'; import 'package:postgres/postgres.dart'; @@ -218,31 +216,31 @@ void main() { test("Escape strings", () { final encoder = new PostgresTextEncoder(true); // ' b o b ' - expect(convert.utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); + expect(utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); // ' b o \n b ' - expect(convert.utf8.encode(encoder.convert('bo\nb')), equals([39, 98, 111, 10, 98, 39])); + expect(utf8.encode(encoder.convert('bo\nb')), equals([39, 98, 111, 10, 98, 39])); // ' b o \r b ' - expect(convert.utf8.encode(encoder.convert('bo\rb')), equals([39, 98, 111, 13, 98, 39])); + expect(utf8.encode(encoder.convert('bo\rb')), equals([39, 98, 111, 13, 98, 39])); // ' b o \b b ' - expect(convert.utf8.encode(encoder.convert('bo\bb')), equals([39, 98, 111, 8, 98, 39])); + expect(utf8.encode(encoder.convert('bo\bb')), equals([39, 98, 111, 8, 98, 39])); // ' ' ' ' - expect(convert.utf8.encode(encoder.convert("'")), equals([39, 39, 39, 39])); + expect(utf8.encode(encoder.convert("'")), equals([39, 39, 39, 39])); // ' ' ' ' ' ' - expect(convert.utf8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); + expect(utf8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); // ' ' ' ' ' ' - expect(convert.utf8.encode(encoder.convert("\''")), equals([39, 39, 39, 39, 39, 39])); + expect(utf8.encode(encoder.convert("\''")), equals([39, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' ' ' - expect(convert.utf8.encode(encoder.convert("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + expect(utf8.encode(encoder.convert("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' - expect(convert.utf8.encode(encoder.convert("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); + expect(utf8.encode(encoder.convert("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); }); test("Encode DateTime", () { @@ -257,13 +255,13 @@ void main() { ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; var pairs = { - "2001-02-03T00:00:00.000$tzOffsetDelimiter": new DateTime(2001, core.DateTime.february, 3), - "2001-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(2001, core.DateTime.february, 3, 4, 5, 6, 0), - "2001-02-03T04:05:06.999$tzOffsetDelimiter": new DateTime(2001, core.DateTime.february, 3, 4, 5, 6, 999), - "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": new DateTime(-10, core.DateTime.february, 3, 4, 5, 6, 123), - "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-10, core.DateTime.february, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-12345, core.DateTime.february, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(12345, core.DateTime.february, 3, 4, 5, 6, 0) + "2001-02-03T00:00:00.000$tzOffsetDelimiter": new DateTime(2001, DateTime.february, 3), + "2001-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(2001, DateTime.february, 3, 4, 5, 6, 0), + "2001-02-03T04:05:06.999$tzOffsetDelimiter": new DateTime(2001, DateTime.february, 3, 4, 5, 6, 999), + "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": new DateTime(-10, DateTime.february, 3, 4, 5, 6, 123), + "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-10, DateTime.february, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-12345, DateTime.february, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(12345, DateTime.february, 3, 4, 5, 6, 0) }; final encoder = new PostgresTextEncoder(false); @@ -274,11 +272,11 @@ void main() { test("Encode Double", () { var pairs = { - "'nan'": core.double.nan, - "'infinity'": core.double.infinity, - "'-infinity'": core.double.negativeInfinity, - "1.7976931348623157e+308": core.double.maxFinite, - "5e-324": core.double.minPositive, + "'nan'": double.nan, + "'infinity'": double.infinity, + "'-infinity'": double.negativeInfinity, + "1.7976931348623157e+308": double.maxFinite, + "5e-324": double.minPositive, "-0.0": -0.0, "0.0": 0.0 }; diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 9082802..6203950 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -1,4 +1,4 @@ -import 'package:dart2_constant/convert.dart' as convert; +import 'dart:convert'; import 'package:postgres/postgres.dart'; import 'package:postgres/src/query.dart'; @@ -100,7 +100,7 @@ void main() { .substitute("@id:text @foo", {"id": "1';select", "foo": "3\\4"}); // ' 1 ' ' ; s e l e c t ' sp sp E ' 3 \ \ 4 ' - expect(convert.utf8.encode(result), [ + expect(utf8.encode(result), [ 39, 49, 39, diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 8e2e2a0..b9d8a8e 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -197,28 +197,88 @@ void main() { }); test( - "A transaction doesn't have to await on queries, when the last query fails, " - "it still emits an error from the transaction", () async { + "A transaction doesn't have to await on queries, when the last query fails, it still emits an error from the transaction", + () async { + var transactionError; await conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)"); ctx.query("INSERT INTO t (id) VALUES (2)"); - ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((_) => null); - }); + ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((e) {}); + }).catchError((e) => transactionError = e); + + expect(transactionError, isNotNull); var total = await conn.query("SELECT id FROM t"); expect(total, []); }); - test("A transaction with a rollback and non-await queries rolls back transaction", () async { + test( + "A transaction doesn't have to await on queries, when the non-last query fails, it still emits an error from the transaction", + () async { + var failingQueryError; + var pendingQueryError; + var transactionError; await conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)"); - ctx.query("INSERT INTO t (id) VALUES (2)"); + ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((e) { + failingQueryError = e; + }); + ctx.query("INSERT INTO t (id) VALUES (2)").catchError((e) { + pendingQueryError = e; + }); + }).catchError((e) => transactionError = e); + expect(transactionError, isNotNull); + expect(failingQueryError.toString(), contains("invalid input")); + expect(pendingQueryError.toString(), contains("failed prior to execution")); + var total = await conn.query("SELECT id FROM t"); + expect(total, []); + }); + + test("A transaction with a rollback and non-await queries rolls back transaction", () async { + var errs = []; + await conn.transaction((ctx) async { + ctx.query("INSERT INTO t (id) VALUES (1)").catchError((e) { + errs.add(e); + }); + ctx.query("INSERT INTO t (id) VALUES (2)").catchError((e) { + errs.add(e); + }); ctx.cancelTransaction(); - ctx.query("INSERT INTO t (id) VALUES (3)"); + ctx.query("INSERT INTO t (id) VALUES (3)").catchError((e) {}); }); var total = await conn.query("SELECT id FROM t"); expect(total, []); + + expect(errs.length, 2); + }); + + test("A transaction that mixes awaiting and non-awaiting queries fails gracefully when an awaited query fails", + () async { + var transactionError; + await conn.transaction((ctx) async { + ctx.query("INSERT INTO t (id) VALUES (1)"); + await ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((_) {}); + ctx.query("INSERT INTO t (id) VALUES (2)").catchError((_) {}); + }).catchError((e) => transactionError = e); + + expect(transactionError, isNotNull); + var total = await conn.query("SELECT id FROM t"); + expect(total, []); + }); + + test("A transaction that mixes awaiting and non-awaiting queries fails gracefully when an unawaited query fails", + () async { + var transactionError; + await conn.transaction((ctx) async { + await ctx.query("INSERT INTO t (id) VALUES (1)"); + ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((_) {}); + await ctx.query("INSERT INTO t (id) VALUES (2)").catchError((_) {}); + }).catchError((e) => transactionError = e); + + expect(transactionError, isNotNull); + var total = await conn.query("SELECT id FROM t"); + expect(total, []); }); }); From fbb4da53fa3df84e5684d21672cd2c4e6751fbf8 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Mon, 6 Aug 2018 11:24:51 -0400 Subject: [PATCH 34/73] Update pubspec for release (#55) * update pubspec for release * remove prerelease env var * stable * disable coverage --- .travis.yml | 8 ++++---- ci/script.sh | 12 ++++-------- pubspec.yaml | 6 +++--- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f04b32..b61b02e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: dart sudo: required dart: - - dev + - stable addons: - postgresql: "9.5" + postgresql: "9.6" services: - postgresql before_script: - - sudo cp ci/pg_hba.conf /etc/postgresql/9.5/main/pg_hba.conf + - sudo cp ci/pg_hba.conf /etc/postgresql/9.6/main/pg_hba.conf - sudo /etc/init.d/postgresql restart - psql -c 'create database dart_test;' -U postgres - psql -c 'create user dart with createdb;' -U postgres @@ -17,7 +17,7 @@ before_script: - psql -c 'grant all on database dart_test to darttrust;' -U postgres - pub get script: bash ci/script.sh -after_success: bash ci/after_script.sh +#after_success: bash ci/after_script.sh branches: only: - master \ No newline at end of file diff --git a/ci/script.sh b/ci/script.sh index 5977cc9..356e7dc 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -1,13 +1,9 @@ #!/bin/bash set -e -export DART_VM_OPTIONS=--preview-dart-2 - pub run test -j 1 -r expanded -export DART_VM_OPTIONS="" - -if [[ "$TRAVIS_BRANCH" == "master" ]]; then - pub global activate -sgit https://github.com/stablekernel/codecov_dart.git - dart_codecov_generator --report-on=lib/ --verbose --no-html -fi +# if [[ "$TRAVIS_BRANCH" == "master" ]]; then +# pub global activate -sgit https://github.com/stablekernel/codecov_dart.git +# dart_codecov_generator --report-on=lib/ --verbose --no-html +# fi diff --git a/pubspec.yaml b/pubspec.yaml index 9206792..286f4dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,16 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 1.0.0-beta.1 +version: 1.0.0 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: environment: - sdk: ">=2.0.0-dev <3.0.0" + sdk: ">=2.0.0 <3.0.0" dependencies: crypto: ^2.0.0 dev_dependencies: - test: '>=0.12.0 <0.13.0' + test: ^1.3.0 coverage: any From c2a94320ea1be5a56f2b3f3555645e3b5bac3c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Tue, 2 Oct 2018 18:06:13 +0200 Subject: [PATCH 35/73] Prevent the table name resolution of OIDs <= 0. (#60) --- CHANGELOG.md | 6 +++++- lib/src/connection.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4890b..17d9f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.1 + +- Prevent the table name resolution of OIDs <= 0. + ## 1.0.0 - Adds support for Dart 2 @@ -11,7 +15,7 @@ ## 0.9.8 - Preserve error stacktrace on various query or transaction errors. -- Read support for `BYEA` columns. +- Read support for `BYTEA` columns. ## 0.9.7 diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 19f84bc..5276737 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -352,7 +352,7 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo // assigning resolvedTableName final tableOIDs = new Set.from(columns.map((f) => f.tableID)); final List unresolvedTableOIDs = - tableOIDs.where((oid) => oid != null && !_tableOIDNameMap.containsKey(oid)).toList(); + tableOIDs.where((oid) => oid != null && oid > 0 && !_tableOIDNameMap.containsKey(oid)).toList(); unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); if (unresolvedTableOIDs.isNotEmpty) { diff --git a/pubspec.yaml b/pubspec.yaml index 286f4dc..3d0b7cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 1.0.0 +version: 1.0.1 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: From 607c661ad1134b595686854c12598e77f95814ef Mon Sep 17 00:00:00 2001 From: Ilya Klyaus Date: Tue, 9 Oct 2018 15:50:48 +0300 Subject: [PATCH 36/73] add connection queue size (#62) * add connection queue size * add queue size test --- CHANGELOG.md | 3 +++ lib/src/connection.dart | 2 ++ lib/src/execution_context.dart | 3 +++ pubspec.yaml | 2 +- test/connection_test.dart | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d9f5b..2161928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 1.0.2 +- Add connection queue size + ## 1.0.1 - Prevent the table name resolution of OIDs <= 0. diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 5276737..3921423 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -302,6 +302,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo PostgreSQLExecutionContext get _transaction; + int get queueSize => _queue.length; + Future>> query(String fmtString, {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds: 30}) async { if (_connection.isClosed) { diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 7c60dc9..79c5066 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -5,6 +5,9 @@ import 'substituter.dart'; import 'connection.dart'; abstract class PostgreSQLExecutionContext { + /// Returns this context queue size + int get queueSize; + /// Executes a query on this context. /// /// This method sends the query described by [fmtString] to the database and returns a [Future] whose value is the returned rows from the query after the query completes. diff --git a/pubspec.yaml b/pubspec.yaml index 3d0b7cd..c1d9e09 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 1.0.1 +version: 1.0.2 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: diff --git a/test/connection_test.dart b/test/connection_test.dart index e662902..fef74bc 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -567,6 +567,22 @@ void main() { } }); + + test("Queue size, should be 0 on open, >0 if queries added and 0 again after queries executed", () async { + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + await conn.open(); + expect(conn.queueSize, 0); + + var futures = [ + conn.query("select 1", allowReuse: false), + conn.query("select 2", allowReuse: false), + conn.query("select 3", allowReuse: false) + ]; + expect(conn.queueSize, 3); + + await Future.wait(futures); + expect(conn.queueSize, 0); + }); } Future expectConnectionIsInvalid(PostgreSQLConnection conn) async { From 8fb0a34fe550cbe8e88b787c99791d1186ea49c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Tue, 30 Oct 2018 17:26:35 +0100 Subject: [PATCH 37/73] Restricted field access on PostgreSQLConnection. (#64) --- CHANGELOG.md | 4 ++++ lib/src/connection.dart | 21 +++++++++++---------- lib/src/connection_fsm.dart | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2161928..f230802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.3 + +- Restricted field access on [PostgreSQLConnection]. + ## 1.0.2 - Add connection queue size diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 3921423..5dd7510 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -43,31 +43,31 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin final StreamController _notifications = new StreamController.broadcast(); /// Hostname of database this connection refers to. - String host; + final String host; /// Port of database this connection refers to. - int port; + final int port; /// Name of database this connection refers to. - String databaseName; + final String databaseName; /// Username for authenticating this connection. - String username; + final String username; /// Password for authenticating this connection. - String password; + final String password; /// Whether or not this connection should connect securely. - bool useSSL; + final bool useSSL; /// The amount of time this connection will wait during connecting before giving up. - int timeoutInSeconds; + final int timeoutInSeconds; /// The timezone of this connection for date operations that don't specify a timezone. - String timeZone; + final String timeZone; /// The processID of this backend. - int processID; + int get processID => _processID; /// Stream of notification from the database. /// @@ -88,11 +88,12 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// /// After connecting to a database, this map will contain the settings values that the database returns. /// Prior to connection, it is the empty map. - Map settings = {}; + final Map settings = {}; QueryCache _cache = new QueryCache(); Socket _socket; MessageFramer _framer = new MessageFramer(); + int _processID; int _secretKey; List _salt; diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index f94e671..8d04550 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -108,8 +108,8 @@ class _PostgreSQLConnectionStateAuthenticating extends _PostgreSQLConnectionStat if (message is ParameterStatusMessage) { connection.settings[message.name] = message.value; } else if (message is BackendKeyMessage) { + connection._processID = message.processID; connection._secretKey = message.secretKey; - connection.processID = message.processID; } else if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateIdle) { return new _PostgreSQLConnectionStateIdle(openCompleter: completer); @@ -141,8 +141,8 @@ class _PostgreSQLConnectionStateAuthenticated extends _PostgreSQLConnectionState if (message is ParameterStatusMessage) { connection.settings[message.name] = message.value; } else if (message is BackendKeyMessage) { + connection._processID = message.processID; connection._secretKey = message.secretKey; - connection.processID = message.processID; } else if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateIdle) { return new _PostgreSQLConnectionStateIdle(openCompleter: completer); From 14b9d03c8218f00ba9e041a03f7a60519c2bb5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 3 Nov 2018 01:58:33 +0100 Subject: [PATCH 38/73] Connection-level default query timeout. (#67) --- CHANGELOG.md | 1 + lib/src/connection.dart | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f230802..e03c41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.0.3 - Restricted field access on [PostgreSQLConnection]. +- Connection-level default query timeout. ## 1.0.2 - Add connection queue size diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 5dd7510..2ae7fee 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -32,10 +32,11 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// [databaseName] is the name of the database to connect to. /// [username] and [password] are optional if the database requires user authentication. /// [timeoutInSeconds] refers to the amount of time [PostgreSQLConnection] will wait while establishing a connection before it gives up. + /// [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: null, this.password: null, this.timeoutInSeconds: 30, this.timeZone: "UTC", this.useSSL: false}) { + {this.username: null, this.password: null, this.timeoutInSeconds: 30, this.queryTimeoutInSeconds: 30, this.timeZone: "UTC", this.useSSL: false}) { _connectionState = new _PostgreSQLConnectionStateClosed(); _connectionState.connection = this; } @@ -63,6 +64,9 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// The amount of time this connection will wait during connecting before giving up. final int timeoutInSeconds; + /// The default timeout for [PostgreSQLExecutionContext]'s execute and query methods. + final int queryTimeoutInSeconds; + /// The timezone of this connection for date operations that don't specify a timezone. final String timeZone; @@ -306,7 +310,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo int get queueSize => _queue.length; Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds: 30}) async { + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}) async { + timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } @@ -320,7 +325,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo } Future>>> mappedResultsQuery(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds: 30}) async { + {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}) async { + timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } @@ -335,7 +341,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo return _mapifyRows(rows, query.fieldDescriptions); } - Future execute(String fmtString, {Map substitutionValues: null, int timeoutInSeconds: 30}) { + Future execute(String fmtString, {Map substitutionValues: null, int timeoutInSeconds}) { + timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } From 95ac5f9bbffb91ac549a552c66bb12294b383819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sun, 18 Nov 2018 22:21:04 +0100 Subject: [PATCH 39/73] Option to specify timeout for the transaction's COMMIT query. (#68) --- CHANGELOG.md | 1 + lib/src/connection.dart | 8 ++++++-- lib/src/transaction_proxy.dart | 7 ++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e03c41f..e5f968d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Restricted field access on [PostgreSQLConnection]. - Connection-level default query timeout. +- Option to specify timeout for the transaction's `"COMMIT"` query. ## 1.0.2 - Add connection queue size diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 2ae7fee..d6d93c6 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -176,12 +176,16 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// ctx.query("INSERT INTO t (id) VALUES (2)"); /// } /// }); - Future transaction(Future queryBlock(PostgreSQLExecutionContext connection)) async { + /// + /// If specified, the final `"COMMIT"` query of the transaction will use + /// [commitTimeoutInSeconds] as its timeout, otherwise the connection's + /// default query timeout will be used. + Future transaction(Future queryBlock(PostgreSQLExecutionContext connection), {int commitTimeoutInSeconds}) async { if (isClosed) { throw new PostgreSQLException("Attempting to execute query, but connection is not open."); } - var proxy = new _TransactionProxy(this, queryBlock); + var proxy = new _TransactionProxy(this, queryBlock, commitTimeoutInSeconds); await _enqueue(proxy.beginQuery); diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index 69c9b6a..e39ad52 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -3,7 +3,7 @@ part of postgres.connection; typedef Future _TransactionQuerySignature(PostgreSQLExecutionContext connection); class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { - _TransactionProxy(this._connection, this.executionBlock) { + _TransactionProxy(this._connection, this.executionBlock, this.commitTimeoutInSeconds) { beginQuery = new Query("BEGIN", {}, _connection, this)..onlyReturnAffectedRowCount = true; beginQuery.future.then(startTransaction).catchError((err, st) { @@ -22,7 +22,8 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp PostgreSQLExecutionContext get _transaction => this; - _TransactionQuerySignature executionBlock; + final _TransactionQuerySignature executionBlock; + final int commitTimeoutInSeconds; bool _hasFailed = false; bool _hasRolledBack = false; @@ -56,7 +57,7 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp } if (!_hasRolledBack && !_hasFailed) { - await execute("COMMIT"); + await execute("COMMIT", timeoutInSeconds: commitTimeoutInSeconds); completer.complete(result); } } From 835525542fb483ca0e0cc3319b75fef19cb91daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 17 Dec 2018 04:02:51 +0100 Subject: [PATCH 40/73] Minor optimization: List -> Queue (#71) * Minor optimization: List -> Queue * Fixing test. --- lib/src/message_window.dart | 8 +++++--- test/framer_test.dart | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 4125356..e466d2e 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -1,5 +1,7 @@ -import 'dart:typed_data'; +import 'dart:collection'; import 'dart:io'; +import 'dart:typed_data'; + import 'server_messages.dart'; class MessageFrame { @@ -132,7 +134,7 @@ class MessageFrame { class MessageFramer { MessageFrame messageInProgress = new MessageFrame(); - List messageQueue = []; + final messageQueue = new Queue(); void addBytes(Uint8List bytes) { var offsetIntoBytesRead = 0; @@ -151,6 +153,6 @@ class MessageFramer { bool get hasMessage => messageQueue.isNotEmpty; MessageFrame popMessage() { - return messageQueue.removeAt(0); + return messageQueue.removeFirst(); } } diff --git a/test/framer_test.dart b/test/framer_test.dart index 28f0fb1..13cf361 100644 --- a/test/framer_test.dart +++ b/test/framer_test.dart @@ -223,7 +223,7 @@ List bufferWithMessages(List> messages) { } flush(MessageFramer framer) { - framer.messageQueue = []; + framer.messageQueue.clear(); framer.addBytes(bufferWithMessages([ messageWithBytes([1, 2, 3], 1) ])); From 34b1ed76bd558e5feb8eb70efc17a4c1458e7c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Tue, 12 Nov 2019 09:04:00 +0100 Subject: [PATCH 41/73] Enable CI for dev branch. (#76) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b61b02e..172431b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,4 +20,5 @@ script: bash ci/script.sh #after_success: bash ci/after_script.sh branches: only: - - master \ No newline at end of file + - master + - dev From 161bc67f4c9ae32e5f26c9a14152dc89fc83fdd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 14 Mar 2019 22:47:52 +0100 Subject: [PATCH 42/73] Use package:buffer to parse incoming message frames. (#72) --- lib/src/message_window.dart | 119 ++++++------------------------------ pubspec.yaml | 1 + 2 files changed, 21 insertions(+), 99 deletions(-) diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index e466d2e..130f044 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -1,7 +1,8 @@ import 'dart:collection'; -import 'dart:io'; import 'dart:typed_data'; +import 'package:buffer/buffer.dart'; + import 'server_messages.dart'; class MessageFrame { @@ -22,8 +23,6 @@ class MessageFrame { 116: () => new ParameterDescriptionMessage() }; - int get bytesAvailable => packets.fold(0, (sum, v) => sum + v.lengthInBytes); - List packets = []; bool get hasReadHeader => type != null; int type; int expectedLength; @@ -31,97 +30,6 @@ class MessageFrame { bool get isComplete => data != null || expectedLength == 0; Uint8List data; - ByteData consumeNextBytes(int length) { - if (length == 0) { - return null; - } - - if (bytesAvailable >= length) { - var firstPacket = packets.first; - - // The packet exactly matches the size of the bytes needed, - // remove & return it. - if (firstPacket.lengthInBytes == length) { - packets.removeAt(0); - return firstPacket.buffer - .asByteData(firstPacket.offsetInBytes, firstPacket.lengthInBytes); - } - - if (firstPacket.lengthInBytes > length) { - // We have to split up this packet and remove & return the first portion of it, - // and replace it with the second portion of it. - var remainingOffset = firstPacket.offsetInBytes + length; - var bytesNeeded = - firstPacket.buffer.asByteData(firstPacket.offsetInBytes, length); - var bytesRemaining = firstPacket.buffer - .asUint8List(remainingOffset, firstPacket.lengthInBytes - length); - packets.removeAt(0); - packets.insert(0, bytesRemaining); - - return bytesNeeded; - } - - // Otherwise, the first packet can't fill this message, but we know - // we have enough packets overall to fulfill it. So we can build - // a total buffer by accumulating multiple packets into that buffer. - // Each packet gets removed along the way, except for the last one, - // in which case if it has more bytes available, it gets replaced - // with the remaining bytes. - - var builder = new BytesBuilder(copy: false); - var bytesNeeded = length - builder.length; - while (bytesNeeded > 0) { - var packet = packets.removeAt(0); - var bytesRemaining = packet.lengthInBytes; - - if (bytesRemaining <= bytesNeeded) { - builder.add(packet.buffer - .asUint8List(packet.offsetInBytes, packet.lengthInBytes)); - } else { - builder.add( - packet.buffer.asUint8List(packet.offsetInBytes, bytesNeeded)); - packets.insert( - 0, - packet.buffer - .asUint8List(bytesNeeded, bytesRemaining - bytesNeeded)); - } - - bytesNeeded = length - builder.length; - } - - return new Uint8List.fromList(builder.takeBytes()).buffer.asByteData(); - } - - return null; - } - - int addBytes(Uint8List packet) { - packets.add(packet); - - if (!hasReadHeader) { - ByteData headerBuffer = consumeNextBytes(HeaderByteSize); - if (headerBuffer == null) { - return packet.lengthInBytes; - } - - type = headerBuffer.getUint8(0); - expectedLength = headerBuffer.getUint32(1) - 4; - } - - if (expectedLength == 0) { - return packet.lengthInBytes - bytesAvailable; - } - - var body = consumeNextBytes(expectedLength); - if (body == null) { - return packet.lengthInBytes; - } - - data = body.buffer.asUint8List(body.offsetInBytes, body.lengthInBytes); - - return packet.lengthInBytes - bytesAvailable; - } - ServerMessage get message { var msgMaker = messageTypeMap[type] ?? () => new UnknownMessage()..code = type; @@ -133,21 +41,34 @@ class MessageFrame { } class MessageFramer { + final _reader = new ByteDataReader(); MessageFrame messageInProgress = new MessageFrame(); final messageQueue = new Queue(); void addBytes(Uint8List bytes) { - var offsetIntoBytesRead = 0; + _reader.add(bytes); + + bool evaluateNextMessage = true; + while (evaluateNextMessage) { + evaluateNextMessage = false; + if (!messageInProgress.hasReadHeader && + _reader.remainingLength >= MessageFrame.HeaderByteSize) { + messageInProgress.type = _reader.readUint8(); + messageInProgress.expectedLength = _reader.readUint32() - 4; + } - do { - var byteList = new Uint8List.view(bytes.buffer, offsetIntoBytesRead); - offsetIntoBytesRead += messageInProgress.addBytes(byteList); + if (messageInProgress.hasReadHeader && + messageInProgress.expectedLength > 0 && + _reader.remainingLength >= messageInProgress.expectedLength) { + messageInProgress.data = _reader.read(messageInProgress.expectedLength); + } if (messageInProgress.isComplete) { messageQueue.add(messageInProgress); messageInProgress = new MessageFrame(); + evaluateNextMessage = true; } - } while (offsetIntoBytesRead != bytes.length); + } } bool get hasMessage => messageQueue.isNotEmpty; diff --git a/pubspec.yaml b/pubspec.yaml index c1d9e09..326ae3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: sdk: ">=2.0.0 <3.0.0" dependencies: + buffer: ^1.0.5 crypto: ^2.0.0 dev_dependencies: From d746a00f265c4c05f04b38888ddc8aac92206a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 16 Mar 2019 15:19:53 +0100 Subject: [PATCH 43/73] Update ClientMessage(s) to use package:buffer. (#78) --- lib/src/client_messages.dart | 208 ++++++++++++----------------------- 1 file changed, 70 insertions(+), 138 deletions(-) diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 85b852f..88f9db8 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -2,6 +2,8 @@ import 'utf8_backed_string.dart'; import 'dart:typed_data'; import 'query.dart'; import 'constants.dart'; + +import 'package:buffer/buffer.dart'; import 'package:crypto/crypto.dart'; abstract class ClientMessage { @@ -20,44 +22,27 @@ abstract class ClientMessage { int get length; - int applyStringToBuffer( - UTF8BackedString string, ByteData buffer, int offset) { - var postStringOffset = string.utf8Bytes.fold(offset, (idx, unit) { - buffer.setInt8(idx, unit); - return idx + 1; - }); - - buffer.setInt8(postStringOffset, 0); - return postStringOffset + 1; + void applyStringToBuffer(UTF8BackedString string, ByteDataWriter buffer) { + buffer.write(string.utf8Bytes); + buffer.writeInt8(0); } - int applyBytesToBuffer(List bytes, ByteData buffer, int offset) { - var postStringOffset = bytes.fold(offset, (idx, unit) { - buffer.setInt8(idx, unit); - return idx + 1; - }); - - return postStringOffset; + void applyBytesToBuffer(List bytes, ByteDataWriter buffer) { + buffer.write(bytes); } - int applyToBuffer(ByteData aggregateBuffer, int offsetIntoAggregateBuffer); + void applyToBuffer(ByteDataWriter buffer); Uint8List asBytes() { - var buffer = new ByteData(length); - applyToBuffer(buffer, 0); - return buffer.buffer.asUint8List(); + var buffer = new ByteDataWriter(); + applyToBuffer(buffer); + return buffer.toBytes(); } static Uint8List aggregateBytes(List messages) { - var totalLength = - messages.fold(0, (total, ClientMessage next) => total + next.length); - var buffer = new ByteData(totalLength); - - var offset = 0; - messages.fold( - offset, (inOffset, msg) => msg.applyToBuffer(buffer, inOffset)); - - return buffer.buffer.asUint8List(); + var buffer = new ByteDataWriter(); + messages.forEach((cm) => cm.applyToBuffer(buffer)); + return buffer.toBytes(); } } @@ -86,31 +71,25 @@ class StartupMessage extends ClientMessage { return fixedLength + variableLength; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setInt32(offset, length); - offset += 4; - buffer.setInt32(offset, ClientMessage.ProtocolVersion); - offset += 4; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeInt32(length); + buffer.writeInt32(ClientMessage.ProtocolVersion); if (username != null) { - offset = applyBytesToBuffer((UTF8ByteConstants.user), buffer, offset); - offset = applyStringToBuffer(username, buffer, offset); + applyBytesToBuffer((UTF8ByteConstants.user), buffer); + applyStringToBuffer(username, buffer); } - offset = applyBytesToBuffer(UTF8ByteConstants.database, buffer, offset); - offset = applyStringToBuffer(databaseName, buffer, offset); + applyBytesToBuffer(UTF8ByteConstants.database, buffer); + applyStringToBuffer(databaseName, buffer); - offset = - applyBytesToBuffer(UTF8ByteConstants.clientEncoding, buffer, offset); - offset = applyBytesToBuffer(UTF8ByteConstants.utf8, buffer, offset); + applyBytesToBuffer(UTF8ByteConstants.clientEncoding, buffer); + applyBytesToBuffer(UTF8ByteConstants.utf8, buffer); - offset = applyBytesToBuffer(UTF8ByteConstants.timeZone, buffer, offset); - offset = applyStringToBuffer(timeZone, buffer, offset); + applyBytesToBuffer(UTF8ByteConstants.timeZone, buffer); + applyStringToBuffer(timeZone, buffer); - buffer.setInt8(offset, 0); - offset += 1; - - return offset; + buffer.writeInt8(0); } } @@ -129,14 +108,10 @@ class AuthMD5Message extends ClientMessage { return 6 + hashedAuthString.utf8Length; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.PasswordIdentifier); - offset += 1; - buffer.setUint32(offset, length - 1); - offset += 4; - offset = applyStringToBuffer(hashedAuthString, buffer, offset); - - return offset; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.PasswordIdentifier); + buffer.writeUint32(length - 1); + applyStringToBuffer(hashedAuthString, buffer); } } @@ -151,14 +126,10 @@ class QueryMessage extends ClientMessage { return 6 + queryString.utf8Length; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.QueryIdentifier); - offset += 1; - buffer.setUint32(offset, length - 1); - offset += 4; - offset = applyStringToBuffer(queryString, buffer, offset); - - return offset; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.QueryIdentifier); + buffer.writeUint32(length - 1); + applyStringToBuffer(queryString, buffer); } } @@ -175,19 +146,13 @@ class ParseMessage extends ClientMessage { return 9 + statement.utf8Length + statementName.utf8Length; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.ParseIdentifier); - offset += 1; - buffer.setUint32(offset, length - 1); - offset += 4; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.ParseIdentifier); + buffer.writeUint32(length - 1); // Name of prepared statement - offset = applyStringToBuffer(statementName, buffer, offset); - offset = applyStringToBuffer(statement, buffer, offset); // Query string - buffer.setUint16(offset, 0); - // Specifying types - may add this in the future, for now indicating we want the backend to infer. - offset += 2; - - return offset; + applyStringToBuffer(statementName, buffer); + applyStringToBuffer(statement, buffer); // Query string + buffer.writeUint16(0); } } @@ -202,17 +167,11 @@ class DescribeMessage extends ClientMessage { return 7 + statementName.utf8Length; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.DescribeIdentifier); - offset += 1; - buffer.setUint32(offset, length - 1); - offset += 4; - buffer.setUint8(offset, 83); - offset += 1; // Indicate we are referencing a prepared statement - offset = applyStringToBuffer( - statementName, buffer, offset); // Name of prepared statement - - return offset; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.DescribeIdentifier); + buffer.writeUint32(length - 1); + buffer.writeUint8(83); + applyStringToBuffer(statementName, buffer); // Name of prepared statement } } @@ -249,67 +208,48 @@ class BindMessage extends ClientMessage { return _cachedLength; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.BindIdentifier); - offset += 1; - buffer.setUint32(offset, length - 1); - offset += 4; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.BindIdentifier); + buffer.writeUint32(length - 1); // Name of portal - currently unnamed portal. - offset = applyBytesToBuffer([0], buffer, offset); + applyBytesToBuffer([0], buffer); // Name of prepared statement. - offset = applyStringToBuffer(statementName, buffer, offset); + applyStringToBuffer(statementName, buffer); // OK, if we have no specified types at all, we can use 0. If we have all specified types, we can use 1. If we have a mix, we have to individually // call out each type. if (typeSpecCount == parameters.length) { - buffer.setUint16(offset, 1); + buffer.writeUint16(1); // Apply following format code for all parameters by indicating 1 - offset += 2; - buffer.setUint16(offset, ClientMessage.FormatBinary); - offset += 2; // Specify format code for all params is BINARY + buffer.writeUint16(ClientMessage.FormatBinary); } else if (typeSpecCount == 0) { - buffer.setUint16(offset, 1); + buffer.writeUint16(1); // Apply following format code for all parameters by indicating 1 - offset += 2; - buffer.setUint16(offset, ClientMessage.FormatText); - offset += 2; // Specify format code for all params is TEXT + buffer.writeUint16(ClientMessage.FormatText); } else { // Well, we have some text and some binary, so we have to be explicit about each one - buffer.setUint16(offset, parameters.length); - offset += 2; + buffer.writeUint16(parameters.length); parameters.forEach((p) { - buffer.setUint16(offset, + buffer.writeUint16( p.isBinary ? ClientMessage.FormatBinary : ClientMessage.FormatText); - offset += 2; }); } // This must be the number of $n's in the query. - buffer.setUint16(offset, parameters.length); - offset += 2; // Number of parameters specified by query + buffer.writeUint16(parameters.length); parameters.forEach((p) { if (p.bytes == null) { - buffer.setInt32(offset, -1); - offset += 4; + buffer.writeInt32(-1); } else { - buffer.setInt32(offset, p.length); - offset += 4; - - offset = p.bytes.fold(offset, (inOffset, byte) { - buffer.setUint8(inOffset, byte); - return inOffset + 1; - }); + buffer.writeInt32(p.length); + buffer.write(p.bytes); } }); // Result columns - we always want binary for all of them, so specify 1:1. - buffer.setUint16(offset, 1); - offset += 2; // Apply format code for all result values by indicating 1 - buffer.setUint16(offset, 1); - offset += 2; // Specify format code for all result values in binary - - return offset; + buffer.writeUint16(1); + buffer.writeUint16(1); } } @@ -320,16 +260,11 @@ class ExecuteMessage extends ClientMessage { return 10; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.ExecuteIdentifier); - offset += 1; - buffer.setUint32(offset, length - 1); - offset += 4; - offset = applyBytesToBuffer([0], buffer, offset); // Portal name - buffer.setUint32(offset, 0); - offset += 4; // Row limit - - return offset; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.ExecuteIdentifier); + buffer.writeUint32(length - 1); + applyBytesToBuffer([0], buffer); // Portal name + buffer.writeUint32(0); } } @@ -340,11 +275,8 @@ class SyncMessage extends ClientMessage { return 5; } - int applyToBuffer(ByteData buffer, int offset) { - buffer.setUint8(offset, ClientMessage.SyncIdentifier); - offset += 1; - buffer.setUint32(offset, 4); - offset += 4; - return offset; + void applyToBuffer(ByteDataWriter buffer) { + buffer.writeUint8(ClientMessage.SyncIdentifier); + buffer.writeUint32(4); } } From adb990ad38e988429bccee415fa0184685f7495e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Wed, 20 Mar 2019 19:34:20 +0100 Subject: [PATCH 44/73] Update CI: run dartfmt, analyzer and test. (#79) * Update CI: run dartfmt, analyzer and test. * running dartfmt --- .travis.yml | 7 +- analysis_options.yaml | 3 +- lib/src/binary_codec.dart | 100 +++++++++----- lib/src/connection.dart | 121 +++++++++++------ lib/src/connection_fsm.dart | 44 ++++--- lib/src/exceptions.dart | 4 +- lib/src/execution_context.dart | 16 ++- lib/src/query.dart | 51 +++++--- lib/src/query_cache.dart | 2 +- lib/src/query_queue.dart | 6 +- lib/src/server_messages.dart | 12 +- lib/src/substituter.dart | 15 ++- lib/src/text_codec.dart | 3 +- lib/src/transaction_proxy.dart | 16 ++- lib/src/types.dart | 2 +- test/connection_test.dart | 231 ++++++++++++++++++++------------- test/decode_test.dart | 28 ++-- test/encoding_test.dart | 140 ++++++++++++-------- test/interpolation_test.dart | 58 +++++---- test/json_test.dart | 133 ++++++++++++++----- test/map_return_test.dart | 43 ++++-- test/notification_test.dart | 54 +++----- test/query_reuse_test.dart | 6 +- test/query_test.dart | 47 +++---- test/timeout_test.dart | 27 +++- test/transaction_test.dart | 55 +++++--- 26 files changed, 774 insertions(+), 450 deletions(-) diff --git a/.travis.yml b/.travis.yml index 172431b..f2e6fdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,12 @@ before_script: - psql -c 'create user darttrust with createdb;' -U postgres - psql -c 'grant all on database dart_test to darttrust;' -U postgres - pub get -script: bash ci/script.sh +#script: bash ci/script.sh +dart_task: + - test: --run-skipped -r expanded -j 1 + - dartfmt + - dartanalyzer: --fatal-infos --fatal-warnings . + #after_success: bash ci/after_script.sh branches: only: diff --git a/analysis_options.yaml b/analysis_options.yaml index 4205311..2c63e94 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,7 +4,8 @@ # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer analyzer: - strong-mode: true +# strong-mode: +# implicit-casts: false # excludes: # - path/to/excluded/files/** diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index cc4c2eb..1b061f5 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -20,8 +20,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.boolean: { if (value is! bool) { - throw new FormatException("Invalid type for parameter value. Expected: bool Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: bool Got: ${value.runtimeType}"); } var bd = new ByteData(1); @@ -32,8 +32,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.bigInteger: { if (value is! int) { - throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); } var bd = new ByteData(8); @@ -44,8 +44,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.integer: { if (value is! int) { - throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); } var bd = new ByteData(4); @@ -55,8 +55,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.smallInteger: { if (value is! int) { - throw new FormatException("Invalid type for parameter value. Expected: int Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); } var bd = new ByteData(2); @@ -67,8 +67,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.text: { if (value is! String) { - throw new FormatException("Invalid type for parameter value. Expected: String Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: String Got: ${value.runtimeType}"); } return utf8.encode(value); @@ -76,8 +76,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.real: { if (value is! double) { - throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); } var bd = new ByteData(4); @@ -87,8 +87,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.double: { if (value is! double) { - throw new FormatException("Invalid type for parameter value. Expected: double Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); } var bd = new ByteData(8); @@ -98,20 +98,21 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.date: { if (value is! DateTime) { - throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); } var bd = new ByteData(4); - bd.setInt32(0, value.toUtc().difference(new DateTime.utc(2000)).inDays); + bd.setInt32( + 0, value.toUtc().difference(new DateTime.utc(2000)).inDays); return bd.buffer.asUint8List(); } case PostgreSQLDataType.timestampWithoutTimezone: { if (value is! DateTime) { - throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); } var bd = new ByteData(8); @@ -123,12 +124,13 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.timestampWithTimezone: { if (value is! DateTime) { - throw new FormatException("Invalid type for parameter value. Expected: DateTime Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); } var bd = new ByteData(8); - bd.setInt64(0, value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); + bd.setInt64(0, + value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); return bd.buffer.asUint8List(); } @@ -147,8 +149,8 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.byteArray: { if (value is! List) { - throw new FormatException("Invalid type for parameter value. Expected: List Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: List Got: ${value.runtimeType}"); } return new Uint8List.fromList(value); } @@ -156,12 +158,16 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.uuid: { if (value is! String) { - throw new FormatException("Invalid type for parameter value. Expected: String Got: ${value - .runtimeType}"); + throw new FormatException( + "Invalid type for parameter value. Expected: String Got: ${value.runtimeType}"); } final dashUnit = "-".codeUnits.first; - final hexBytes = (value as String).toLowerCase().codeUnits.where((c) => c != dashUnit).toList(); + final hexBytes = (value as String) + .toLowerCase() + .codeUnits + .where((c) => c != dashUnit) + .toList(); if (hexBytes.length != 32) { throw new FormatException( "Invalid UUID string. There must be exactly 32 hexadecimal (0-9 and a-f) characters and any number of '-' characters."); @@ -174,7 +180,8 @@ class PostgresBinaryEncoder extends Converter { return charCode - 87; } - throw new FormatException("Invalid UUID string. Contains non-hexadecimal character (0-9 and a-f)."); + throw new FormatException( + "Invalid UUID string. Contains non-hexadecimal character (0-9 and a-f)."); }; final outBuffer = new Uint8List(16); @@ -205,12 +212,14 @@ class PostgresBinaryDecoder extends Converter { return null; } - final buffer = new ByteData.view(value.buffer, value.offsetInBytes, value.lengthInBytes); + final buffer = new ByteData.view( + value.buffer, value.offsetInBytes, value.lengthInBytes); switch (dataType) { case PostgreSQLDataType.name: case PostgreSQLDataType.text: - return utf8.decode(value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); + return utf8.decode( + value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); case PostgreSQLDataType.boolean: return buffer.getInt8(0) != 0; case PostgreSQLDataType.smallInteger: @@ -227,26 +236,47 @@ class PostgresBinaryDecoder extends Converter { return buffer.getFloat64(0); case PostgreSQLDataType.timestampWithoutTimezone: case PostgreSQLDataType.timestampWithTimezone: - return new DateTime.utc(2000).add(new Duration(microseconds: buffer.getInt64(0))); + return new DateTime.utc(2000) + .add(new Duration(microseconds: buffer.getInt64(0))); case PostgreSQLDataType.date: - return new DateTime.utc(2000).add(new Duration(days: buffer.getInt32(0))); + return new DateTime.utc(2000) + .add(new Duration(days: buffer.getInt32(0))); case PostgreSQLDataType.json: { // Removes version which is first character and currently always '1' - final bytes = value.buffer.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1); + final bytes = value.buffer + .asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1); return json.decode(utf8.decode(bytes)); } case PostgreSQLDataType.byteArray: - return value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes); + return value.buffer + .asUint8List(value.offsetInBytes, value.lengthInBytes); case PostgreSQLDataType.uuid: { final codeDash = "-".codeUnitAt(0); - final cipher = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + final cipher = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f' + ]; final byteConvert = (int value) { return cipher[value]; }; diff --git a/lib/src/connection.dart b/lib/src/connection.dart index d6d93c6..72dfe81 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -24,7 +24,9 @@ part 'exceptions.dart'; /// /// The primary type of this library, a connection is responsible for connecting to databases and executing queries. /// A connection may be opened with [open] after it is created. -class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { +class PostgreSQLConnection extends Object + with _PostgreSQLExecutionContextMixin + implements PostgreSQLExecutionContext { /// Creates an instance of [PostgreSQLConnection]. /// /// [host] must be a hostname, e.g. "foobar.com" or IP address. Do not include scheme or port. @@ -36,12 +38,18 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// [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: null, this.password: null, this.timeoutInSeconds: 30, this.queryTimeoutInSeconds: 30, this.timeZone: "UTC", this.useSSL: false}) { + {this.username: null, + this.password: null, + this.timeoutInSeconds: 30, + this.queryTimeoutInSeconds: 30, + this.timeZone: "UTC", + this.useSSL: false}) { _connectionState = new _PostgreSQLConnectionStateClosed(); _connectionState.connection = this; } - final StreamController _notifications = new StreamController.broadcast(); + final StreamController _notifications = + new StreamController.broadcast(); /// Hostname of database this connection refers to. final String host; @@ -98,6 +106,7 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin Socket _socket; MessageFramer _framer = new MessageFramer(); int _processID; + // ignore: unused_field int _secretKey; List _salt; @@ -118,12 +127,14 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// opened and this method is called, an exception will be thrown. Future open() async { if (_hasConnectedPreviously) { - throw new PostgreSQLException("Attempting to reopen a closed connection. Create a new instance instead."); + throw new PostgreSQLException( + "Attempting to reopen a closed connection. Create a new instance instead."); } try { _hasConnectedPreviously = true; - _socket = await Socket.connect(host, port).timeout(new Duration(seconds: timeoutInSeconds)); + _socket = await Socket.connect(host, port) + .timeout(new Duration(seconds: timeoutInSeconds)); _framer = new MessageFramer(); if (useSSL) { @@ -131,13 +142,17 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin } var connectionComplete = new Completer(); - _socket.listen(_readData, onError: (err, st) => _close(err, st), onDone: () => _close()); + _socket.listen(_readData, + onError: (err, st) => _close(err, st), onDone: () => _close()); - _transitionToState(new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); + _transitionToState( + new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); - await connectionComplete.future.timeout(new Duration(seconds: timeoutInSeconds)); + await connectionComplete.future + .timeout(new Duration(seconds: timeoutInSeconds)); } on TimeoutException catch (e, st) { - final err = new PostgreSQLException("Failed to connect to database $host:$port/$databaseName failed to connect."); + final err = new PostgreSQLException( + "Failed to connect to database $host:$port/$databaseName failed to connect."); await _close(err, st); rethrow; } catch (e, st) { @@ -180,9 +195,12 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin /// If specified, the final `"COMMIT"` query of the transaction will use /// [commitTimeoutInSeconds] as its timeout, otherwise the connection's /// default query timeout will be used. - Future transaction(Future queryBlock(PostgreSQLExecutionContext connection), {int commitTimeoutInSeconds}) async { + Future transaction( + Future queryBlock(PostgreSQLExecutionContext connection), + {int commitTimeoutInSeconds}) async { if (isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } var proxy = new _TransactionProxy(this, queryBlock, commitTimeoutInSeconds); @@ -234,7 +252,8 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin if (msg is ErrorResponseMessage) { _transitionToState(_connectionState.onErrorResponse(msg)); } else if (msg is NotificationResponseMessage) { - _notifications.add(new Notification(msg.processID, msg.channel, msg.payload)); + _notifications + .add(new Notification(msg.processID, msg.channel, msg.payload)); } else { _transitionToState(_connectionState.onMessage(msg)); } @@ -250,15 +269,15 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin originalSocket.listen( (data) { if (data.length != 1) { - sslCompleter.completeError( - new PostgreSQLException("Could not initalize SSL connection, received unknown byte stream.")); + sslCompleter.completeError(new PostgreSQLException( + "Could not initalize SSL connection, received unknown byte stream.")); return; } sslCompleter.complete(data.first); }, - onDone: () => sslCompleter.completeError( - new PostgreSQLException("Could not initialize SSL connection, connection closed during handshake.")), + onDone: () => sslCompleter.completeError(new PostgreSQLException( + "Could not initialize SSL connection, connection closed during handshake.")), onError: (err) { sslCompleter.completeError(err); }); @@ -268,13 +287,16 @@ class PostgreSQLConnection extends Object with _PostgreSQLExecutionContextMixin byteBuffer.setUint32(4, 80877103); originalSocket.add(byteBuffer.buffer.asUint8List()); - return sslCompleter.future.timeout(new Duration(seconds: timeout)).then((responseByte) { + return sslCompleter.future + .timeout(new Duration(seconds: timeout)) + .then((responseByte) { if (responseByte != 83) { - throw new PostgreSQLException("The database server is not accepting SSL connections."); + throw new PostgreSQLException( + "The database server is not accepting SSL connections."); } - return SecureSocket - .secure(originalSocket, onBadCertificate: (certificate) => true) + return SecureSocket.secure(originalSocket, + onBadCertificate: (certificate) => true) .timeout(new Duration(seconds: timeout)); }); } @@ -303,7 +325,8 @@ class Notification { final String payload; } -abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { +abstract class _PostgreSQLExecutionContextMixin + implements PostgreSQLExecutionContext { Map _tableOIDNameMap = {}; QueryQueue _queue = new QueryQueue(); @@ -314,13 +337,17 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo int get queueSize => _queue.length; Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}) async { + {Map substitutionValues: null, + bool allowReuse: true, + int timeoutInSeconds}) async { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } - var query = new Query>>(fmtString, substitutionValues, _connection, _transaction); + var query = new Query>>( + fmtString, substitutionValues, _connection, _transaction); if (allowReuse) { query.statementIdentifier = _connection._cache.identifierForQuery(query); } @@ -328,14 +355,19 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } - Future>>> mappedResultsQuery(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}) async { + Future>>> mappedResultsQuery( + String fmtString, + {Map substitutionValues: null, + bool allowReuse: true, + int timeoutInSeconds}) async { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } - var query = new Query>>(fmtString, substitutionValues, _connection, _transaction); + var query = new Query>>( + fmtString, substitutionValues, _connection, _transaction); if (allowReuse) { query.statementIdentifier = _connection._cache.identifierForQuery(query); } @@ -345,14 +377,17 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo return _mapifyRows(rows, query.fieldDescriptions); } - Future execute(String fmtString, {Map substitutionValues: null, int timeoutInSeconds}) { + Future execute(String fmtString, + {Map substitutionValues: null, int timeoutInSeconds}) { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { - throw new PostgreSQLException("Attempting to execute query, but connection is not open."); + throw new PostgreSQLException( + "Attempting to execute query, but connection is not open."); } - var query = new Query(fmtString, substitutionValues, _connection, _transaction) - ..onlyReturnAffectedRowCount = true; + var query = + new Query(fmtString, substitutionValues, _connection, _transaction) + ..onlyReturnAffectedRowCount = true; return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } @@ -365,8 +400,10 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName final tableOIDs = new Set.from(columns.map((f) => f.tableID)); - final List unresolvedTableOIDs = - tableOIDs.where((oid) => oid != null && oid > 0 && !_tableOIDNameMap.containsKey(oid)).toList(); + final List unresolvedTableOIDs = tableOIDs + .where((oid) => + oid != null && oid > 0 && !_tableOIDNameMap.containsKey(oid)) + .toList(); unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); if (unresolvedTableOIDs.isNotEmpty) { @@ -379,13 +416,16 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); return rows.map((row) { - var rowMap = new Map>.fromIterable(tableNames, - key: (name) => name, value: (_) => {}); + var rowMap = new Map>.fromIterable( + tableNames, + key: (name) => name, + value: (_) => {}); final iterator = columns.iterator; row.forEach((column) { iterator.moveNext(); - rowMap[iterator.current.resolvedTableName][iterator.current.fieldName] = column; + rowMap[iterator.current.resolvedTableName][iterator.current.fieldName] = + column; }); return rowMap; @@ -394,8 +434,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo Future _resolveTableOIDs(List oids) async { final unresolvedIDString = oids.join(","); - final orderedTableNames = - await query("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + final orderedTableNames = await query( + "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); final iterator = oids.iterator; orderedTableNames.forEach((tableName) { @@ -411,7 +451,8 @@ abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionCo _connection._transitionToState(_connection._connectionState.awake()); try { - final result = await query.future.timeout(new Duration(seconds: timeoutInSeconds)); + final result = + await query.future.timeout(new Duration(seconds: timeoutInSeconds)); _connection._cache.add(query); _queue.remove(query); return result; diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 8d04550..27409ad 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -18,7 +18,8 @@ abstract class _PostgreSQLConnectionState { _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { var exception = new PostgreSQLException._(message.fields); - if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { + if (exception.severity == PostgreSQLSeverity.fatal || + exception.severity == PostgreSQLSeverity.panic) { return new _PostgreSQLConnectionStateClosed(); } @@ -38,14 +39,16 @@ class _PostgreSQLConnectionStateClosed extends _PostgreSQLConnectionState {} Socket connected, prior to any PostgreSQL handshaking - initiates that handshaking */ -class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateSocketConnected + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateSocketConnected(this.completer); Completer completer; _PostgreSQLConnectionState onEnter() { - var startupMessage = - new StartupMessage(connection.databaseName, connection.timeZone, username: connection.username); + var startupMessage = new StartupMessage( + connection.databaseName, connection.timeZone, + username: connection.username); connection._socket.add(startupMessage.asBytes()); @@ -72,8 +75,8 @@ class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionSta return new _PostgreSQLConnectionStateAuthenticating(completer); } - completer.completeError(new PostgreSQLException("Unsupported authentication type ${authMessage - .type}, closing connection.")); + completer.completeError(new PostgreSQLException( + "Unsupported authentication type ${authMessage.type}, closing connection.")); return new _PostgreSQLConnectionStateClosed(); } @@ -83,13 +86,15 @@ class _PostgreSQLConnectionStateSocketConnected extends _PostgreSQLConnectionSta Authenticating state */ -class _PostgreSQLConnectionStateAuthenticating extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateAuthenticating + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateAuthenticating(this.completer); Completer completer; _PostgreSQLConnectionState onEnter() { - var authMessage = new AuthMD5Message(connection.username, connection.password, connection._salt); + var authMessage = new AuthMD5Message( + connection.username, connection.password, connection._salt); connection._socket.add(authMessage.asBytes()); @@ -124,7 +129,8 @@ class _PostgreSQLConnectionStateAuthenticating extends _PostgreSQLConnectionStat Authenticated state */ -class _PostgreSQLConnectionStateAuthenticated extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateAuthenticated + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateAuthenticated(this.completer); Completer completer; @@ -221,7 +227,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { var exception = new PostgreSQLException._(message.fields); returningException ??= exception; - if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { + if (exception.severity == PostgreSQLSeverity.fatal || + exception.severity == PostgreSQLSeverity.panic) { return new _PostgreSQLConnectionStateClosed(); } @@ -232,12 +239,13 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { // We ignore NoData, as it doesn't tell us anything we don't already know // or care about. - // print("(${query.statement}) -> $message"); + // print("(${query.statement}) -> $message"); if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateTransactionError) { query.completeError(returningException); - return new _PostgreSQLConnectionStateReadyInTransaction(query.transaction); + return new _PostgreSQLConnectionStateReadyInTransaction( + query.transaction); } if (returningException != null) { @@ -247,7 +255,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } if (message.state == ReadyForQueryMessage.StateTransaction) { - return new _PostgreSQLConnectionStateReadyInTransaction(query.transaction); + return new _PostgreSQLConnectionStateReadyInTransaction( + query.transaction); } return new _PostgreSQLConnectionStateIdle(); @@ -258,7 +267,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } else if (message is DataRowMessage) { query.addRow(message.values); } else if (message is ParameterDescriptionMessage) { - var validationException = query.validateParameters(message.parameterTypeIDs); + var validationException = + query.validateParameters(message.parameterTypeIDs); if (validationException != null) { query.cache = null; } @@ -271,7 +281,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { /* Idle Transaction State */ -class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnectionState { +class _PostgreSQLConnectionStateReadyInTransaction + extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateReadyInTransaction(this.transaction); _TransactionProxy transaction; @@ -314,4 +325,5 @@ class _PostgreSQLConnectionStateReadyInTransaction extends _PostgreSQLConnection Hack for deferred error */ -class _PostgreSQLConnectionStateDeferredFailure extends _PostgreSQLConnectionState {} +class _PostgreSQLConnectionStateDeferredFailure + extends _PostgreSQLConnectionState {} diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index 153905f..e0a5bc7 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -47,8 +47,8 @@ class PostgreSQLException implements Exception { (ErrorField e) => e.identificationToken == identifer, orElse: () => null)); - severity = ErrorField - .severityFromString(finder(ErrorField.SeverityIdentifier).text); + severity = ErrorField.severityFromString( + finder(ErrorField.SeverityIdentifier).text); code = finder(ErrorField.CodeIdentifier).text; message = finder(ErrorField.MessageIdentifier).text; detail = finder(ErrorField.DetailIdentifier)?.text; diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 79c5066..3ec8bed 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -29,7 +29,9 @@ abstract class PostgreSQLExecutionContext { /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}); + {Map substitutionValues: null, + bool allowReuse: true, + int timeoutInSeconds}); /// Executes a query on this context. /// @@ -38,7 +40,8 @@ abstract class PostgreSQLExecutionContext { /// This method returns the number of rows affected and no additional information. This method uses the least efficient and less secure command /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. - Future execute(String fmtString, {Map substitutionValues: null, int timeoutInSeconds}); + Future execute(String fmtString, + {Map substitutionValues: null, int timeoutInSeconds}); /// Cancels a transaction on this context. /// @@ -76,6 +79,9 @@ abstract class PostgreSQLExecutionContext { /// "company: {"name": "stable|kernel"} /// } /// ] - Future>>> mappedResultsQuery(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}); -} \ No newline at end of file + Future>>> mappedResultsQuery( + String fmtString, + {Map substitutionValues: null, + bool allowReuse: true, + int timeoutInSeconds}); +} diff --git a/lib/src/query.dart b/lib/src/query.dart index 1d045f6..d99d455 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -13,7 +13,8 @@ import 'substituter.dart'; import 'client_messages.dart'; class Query { - Query(this.statement, this.substitutionValues, this.connection, this.transaction); + Query(this.statement, this.substitutionValues, this.connection, + this.transaction); bool onlyReturnAffectedRowCount = false; @@ -67,7 +68,9 @@ class Query { specifiedParameterTypeCodes = formatIdentifiers.map((i) => i.type).toList(); - var parameterList = formatIdentifiers.map((id) => new ParameterValue(id, substitutionValues)).toList(); + var parameterList = formatIdentifiers + .map((id) => new ParameterValue(id, substitutionValues)) + .toList(); var messages = [ new ParseMessage(sqlString, statementName: statementName), @@ -84,27 +87,34 @@ class Query { socket.add(ClientMessage.aggregateBytes(messages)); } - void sendCachedQuery(Socket socket, CachedQuery cacheQuery, Map substitutionValues) { + void sendCachedQuery(Socket socket, CachedQuery cacheQuery, + Map substitutionValues) { var statementName = cacheQuery.preparedStatementName; - var parameterList = - cacheQuery.orderedParameters.map((identifier) => new ParameterValue(identifier, substitutionValues)).toList(); + var parameterList = cacheQuery.orderedParameters + .map((identifier) => new ParameterValue(identifier, substitutionValues)) + .toList(); - var bytes = ClientMessage.aggregateBytes( - [new BindMessage(parameterList, statementName: statementName), new ExecuteMessage(), new SyncMessage()]); + var bytes = ClientMessage.aggregateBytes([ + new BindMessage(parameterList, statementName: statementName), + new ExecuteMessage(), + new SyncMessage() + ]); socket.add(bytes); } PostgreSQLException validateParameters(List parameterTypeIDs) { var actualParameterTypeCodeIterator = parameterTypeIDs.iterator; - var parametersAreMismatched = specifiedParameterTypeCodes.map((specifiedType) { + var parametersAreMismatched = + specifiedParameterTypeCodes.map((specifiedType) { actualParameterTypeCodeIterator.moveNext(); if (specifiedType == null) { return true; } - final actualType = PostgresBinaryDecoder.typeMap[actualParameterTypeCodeIterator.current]; + final actualType = PostgresBinaryDecoder + .typeMap[actualParameterTypeCodeIterator.current]; return actualType == specifiedType; }).any((v) => v == false); @@ -125,7 +135,8 @@ class Query { var lazyDecodedData = rawRowData.map((bd) { iterator.moveNext(); - return iterator.current.converter.convert(bd?.buffer?.asUint8List(bd.offsetInBytes, bd.lengthInBytes)); + return iterator.current.converter + .convert(bd?.buffer?.asUint8List(bd.offsetInBytes, bd.lengthInBytes)); }); rows.add(lazyDecodedData.toList()); @@ -163,20 +174,25 @@ class CachedQuery { List fieldDescriptions; bool get isValid { - return preparedStatementName != null && orderedParameters != null && fieldDescriptions != null; + return preparedStatementName != null && + orderedParameters != null && + fieldDescriptions != null; } } class ParameterValue { - factory ParameterValue(PostgreSQLFormatIdentifier identifier, Map substitutionValues) { + factory ParameterValue(PostgreSQLFormatIdentifier identifier, + Map substitutionValues) { if (identifier.type == null) { return new ParameterValue.text(substitutionValues[identifier.name]); } - return new ParameterValue.binary(substitutionValues[identifier.name], identifier.type); + return new ParameterValue.binary( + substitutionValues[identifier.name], identifier.type); } - ParameterValue.binary(dynamic value, PostgreSQLDataType postgresType) : isBinary = true { + ParameterValue.binary(dynamic value, PostgreSQLDataType postgresType) + : isBinary = true { final converter = new PostgresBinaryEncoder(postgresType); bytes = converter.convert(value); length = bytes?.length ?? 0; @@ -245,7 +261,8 @@ class FieldDescription { } } -typedef String SQLReplaceIdentifierFunction(PostgreSQLFormatIdentifier identifier, int index); +typedef String SQLReplaceIdentifierFunction( + PostgreSQLFormatIdentifier identifier, int index); enum PostgreSQLFormatTokenType { text, variable } @@ -257,7 +274,6 @@ class PostgreSQLFormatToken { } class PostgreSQLFormatIdentifier { - static Map typeStringToCodeMap = { "text": PostgreSQLDataType.text, "int2": PostgreSQLDataType.smallInteger, @@ -291,7 +307,8 @@ class PostgreSQLFormatIdentifier { if (dataTypeString != null) { type = typeStringToCodeMap[dataTypeString]; if (type == null) { - throw new FormatException("Invalid type code in substitution variable '$t'"); + throw new FormatException( + "Invalid type code in substitution variable '$t'"); } } } else { diff --git a/lib/src/query_cache.dart b/lib/src/query_cache.dart index 7c83680..c7fef2c 100644 --- a/lib/src/query_cache.dart +++ b/lib/src/query_cache.dart @@ -34,4 +34,4 @@ class QueryCache { return string; } -} \ No newline at end of file +} diff --git a/lib/src/query_queue.dart b/lib/src/query_queue.dart index 2135f17..53d12d0 100644 --- a/lib/src/query_queue.dart +++ b/lib/src/query_queue.dart @@ -4,11 +4,13 @@ import 'dart:collection'; import 'package:postgres/postgres.dart'; import 'package:postgres/src/query.dart'; -class QueryQueue extends ListBase> implements List> { +class QueryQueue extends ListBase> + implements List> { List> _inner = []; bool _isCancelled = false; - PostgreSQLException get _cancellationException => new PostgreSQLException("Query cancelled due to the database connection closing."); + PostgreSQLException get _cancellationException => new PostgreSQLException( + "Query cancelled due to the database connection closing."); Query get pending { if (_inner.isEmpty) { diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index b6e5668..a50bcab 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -12,7 +12,8 @@ class ErrorResponseMessage implements ServerMessage { List fields = [new ErrorField()]; void readBytes(Uint8List bytes) { - var lastByteRemovedList = new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); + var lastByteRemovedList = + new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); lastByteRemovedList.forEach((byte) { if (byte != 0) { @@ -58,7 +59,8 @@ class ParameterStatusMessage extends ServerMessage { void readBytes(Uint8List bytes) { name = utf8.decode(bytes.sublist(0, bytes.indexOf(0))); - value = utf8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + value = + utf8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); } } @@ -121,7 +123,8 @@ class DataRowMessage extends ServerMessage { } else if (dataSize == -1) { values.add(null); } else { - var rawBytes = new ByteData.view(bytes.buffer, bytes.offsetInBytes + offset, dataSize); + var rawBytes = new ByteData.view( + bytes.buffer, bytes.offsetInBytes + offset, dataSize); values.add(rawBytes); offset += dataSize; } @@ -140,7 +143,8 @@ class NotificationResponseMessage extends ServerMessage { var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); processID = view.getUint32(0); channel = utf8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); - payload = utf8.decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); + payload = utf8 + .decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); } } diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 48b34aa..4682fca 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -66,17 +66,20 @@ class PostgreSQLFormat { while (iterator.current != null) { if (currentPtr == null) { if (iterator.current == _AtSignCodeUnit) { - currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + currentPtr = + new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } else { - currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); + currentPtr = + new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } } else if (currentPtr.type == PostgreSQLFormatTokenType.text) { if (iterator.current == _AtSignCodeUnit) { - currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + currentPtr = + new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } else { @@ -98,7 +101,8 @@ class PostgreSQLFormat { } else if (_isIdentifier(iterator.current)) { currentPtr.buffer.writeCharCode(iterator.current); } else { - currentPtr = new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); + currentPtr = + new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } @@ -118,8 +122,7 @@ class PostgreSQLFormat { if (!values.containsKey(identifier.name)) { throw new FormatException( - "Format string specified identifier with name ${identifier - .name}, but key was not present in values. Format string: $fmtString"); + "Format string specified identifier with name ${identifier.name}, but key was not present in values. Format string: $fmtString"); } var val = replace(identifier, idx); diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index 844c15e..92f914b 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -124,7 +124,8 @@ class PostgresTextEncoder extends Converter { var timezoneMinuteOffset = value.timeZoneOffset.inMinutes % 60; var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, "0"); - var minuteComponent = timezoneMinuteOffset.abs().toString().padLeft(2, "0"); + var minuteComponent = + timezoneMinuteOffset.abs().toString().padLeft(2, "0"); if (timezoneHourOffset >= 0) { hourComponent = "+${hourComponent}"; diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index e39ad52..a661afe 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -1,10 +1,15 @@ part of postgres.connection; -typedef Future _TransactionQuerySignature(PostgreSQLExecutionContext connection); +typedef Future _TransactionQuerySignature( + PostgreSQLExecutionContext connection); -class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { - _TransactionProxy(this._connection, this.executionBlock, this.commitTimeoutInSeconds) { - beginQuery = new Query("BEGIN", {}, _connection, this)..onlyReturnAffectedRowCount = true; +class _TransactionProxy extends Object + with _PostgreSQLExecutionContextMixin + implements PostgreSQLExecutionContext { + _TransactionProxy( + this._connection, this.executionBlock, this.commitTimeoutInSeconds) { + beginQuery = new Query("BEGIN", {}, _connection, this) + ..onlyReturnAffectedRowCount = true; beginQuery.future.then(startTransaction).catchError((err, st) { new Future(() { @@ -79,7 +84,8 @@ class _TransactionProxy extends Object with _PostgreSQLExecutionContextMixin imp "that prevented this query from executing."); _queue.cancel(err); - var rollback = new Query("ROLLBACK", {}, _connection, _transaction)..onlyReturnAffectedRowCount = true; + var rollback = new Query("ROLLBACK", {}, _connection, _transaction) + ..onlyReturnAffectedRowCount = true; _queue.addEvenIfCancelled(rollback); _connection._transitionToState(_connection._connectionState.awake()); diff --git a/lib/src/types.dart b/lib/src/types.dart index 44238af..ee76281 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -65,4 +65,4 @@ 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 -} \ No newline at end of file +} diff --git a/test/connection_test.dart b/test/connection_test.dart index fef74bc..9e6fd3c 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -14,7 +14,8 @@ void main() { }); test("Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); @@ -22,63 +23,67 @@ void main() { }); test("SSL Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart", useSSL: true); await conn.open(); expect(await conn.execute("select 1"), equals(1)); - var socketMirror = reflect(conn) - .type - .declarations - .values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); - var underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_socket")); + var underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee; expect(underlyingSocket is SecureSocket, true); }); test("Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); expect(await conn.execute("select 1"), equals(1)); }); test("SSL Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); await conn.open(); expect(await conn.execute("select 1"), equals(1)); }); - test("Closing idle connection succeeds, closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test("Closing idle connection succeeds, closes underlying socket", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.close(); - var socketMirror = reflect(conn) - .type - .declarations - .values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_socket")); + Socket underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee; expect(await underlyingSocket.done, isNotNull); conn = null; }); - test("SSL Closing idle connection succeeds, closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); + test("SSL Closing idle connection succeeds, closes underlying socket", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); await conn.open(); await conn.close(); - var socketMirror = reflect(conn) - .type - .declarations - .values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; + var socketMirror = reflect(conn).type.declarations.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_socket")); + Socket underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee; expect(await underlyingSocket.done, isNotNull); conn = null; @@ -87,7 +92,8 @@ void main() { test( "Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); var errors = []; @@ -106,13 +112,15 @@ void main() { await conn.close(); await Future.wait(futures); expect(errors.length, 5); - expect(errors.map((e) => e.message), everyElement(contains("Query cancelled"))); + expect(errors.map((e) => e.message), + everyElement(contains("Query cancelled"))); }); test( "SSL Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); await conn.open(); var errors = []; @@ -131,7 +139,8 @@ void main() { await conn.close(); await Future.wait(futures); expect(errors.length, 5); - expect(errors.map((e) => e.message), everyElement(contains("Query cancelled"))); + expect(errors.map((e) => e.message), + everyElement(contains("Query cancelled"))); }); }); @@ -139,7 +148,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); }); @@ -147,7 +157,9 @@ void main() { await conn?.close(); }); - test("Issuing multiple queries and awaiting between each one successfully returns the right value", () async { + test( + "Issuing multiple queries and awaiting between each one successfully returns the right value", + () async { expect( await conn.query("select 1", allowReuse: false), equals([ @@ -175,7 +187,9 @@ void main() { ])); }); - test("Issuing multiple queries without awaiting are returned with appropriate values", () async { + test( + "Issuing multiple queries without awaiting are returned with appropriate values", + () async { var futures = [ conn.query("select 1", allowReuse: false), conn.query("select 2", allowReuse: false), @@ -216,7 +230,8 @@ void main() { }); test("Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); openFuture = conn.open(); try { @@ -228,7 +243,8 @@ void main() { }); test("SSL Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); openFuture = conn.open(); try { @@ -239,8 +255,10 @@ void main() { } }); - test("Starting transaction while opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test("Starting transaction while opening connection triggers error", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); openFuture = conn.open(); try { @@ -253,8 +271,10 @@ void main() { } }); - test("SSL Starting transaction while opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust", useSSL: true); + test("SSL Starting transaction while opening connection triggers error", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust", useSSL: true); openFuture = conn.open(); try { @@ -267,8 +287,10 @@ void main() { } }); - test("Invalid password reports error, conn is closed, disables conn", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "notdart"); + test("Invalid password reports error, conn is closed, disables conn", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "notdart"); try { await conn.open(); @@ -280,9 +302,10 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("SSL Invalid password reports error, conn is closed, disables conn", () async { - conn = - new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "notdart", useSSL: true); + test("SSL Invalid password reports error, conn is closed, disables conn", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "notdart", useSSL: true); try { await conn.open(); @@ -294,8 +317,10 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("A query error maintains connectivity, allows future queries", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test("A query error maintains connectivity, allows future queries", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -310,8 +335,11 @@ void main() { await conn.execute("INSERT INTO t (i) VALUES (2)"); }); - test("A query error maintains connectivity, continues processing pending queries", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "A query error maintains connectivity, continues processing pending queries", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -341,17 +369,19 @@ void main() { ] ]); - var queueMirror = reflect(conn) - .type - .instanceMembers - .values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_queue")); - List queue = reflect(conn).getField(queueMirror.simpleName).reflectee; + var queueMirror = reflect(conn).type.instanceMembers.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_queue")); + List queue = + reflect(conn).getField(queueMirror.simpleName).reflectee; expect(queue, isEmpty); }); - test("A query error maintains connectivity, continues processing pending transactions", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "A query error maintains connectivity, continues processing pending transactions", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); @@ -379,8 +409,11 @@ void main() { expect(orderEnsurer, [2, 1, 3, 4]); }); - test("Building query throws error, connection continues processing pending queries", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "darttrust"); + test( + "Building query throws error, connection continues processing pending queries", + () async { + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "darttrust"); await conn.open(); // Make some async queries that'll exit the event loop, but then fail on a query that'll die early @@ -403,12 +436,11 @@ void main() { ] ]); - var queueMirror = reflect(conn) - .type - .instanceMembers - .values - .firstWhere((DeclarationMirror dm) => dm.simpleName.toString().contains("_queue")); - List queue = reflect(conn).getField(queueMirror.simpleName).reflectee; + var queueMirror = reflect(conn).type.instanceMembers.values.firstWhere( + (DeclarationMirror dm) => + dm.simpleName.toString().contains("_queue")); + List queue = + reflect(conn).getField(queueMirror.simpleName).reflectee; expect(queue, isEmpty); }); }); @@ -422,7 +454,9 @@ void main() { await socket?.close(); }); - test("Socket fails to connect reports error, disables connection for future use", () async { + test( + "Socket fails to connect reports error, disables connection for future use", + () async { var conn = new PostgreSQLConnection("localhost", 5431, "dart_test"); try { @@ -433,8 +467,11 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("SSL Socket fails to connect reports error, disables connection for future use", () async { - var conn = new PostgreSQLConnection("localhost", 5431, "dart_test", useSSL: true); + test( + "SSL Socket fails to connect reports error, disables connection for future use", + () async { + var conn = new PostgreSQLConnection("localhost", 5431, "dart_test", + useSSL: true); try { await conn.open(); @@ -444,15 +481,19 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("Connection that times out throws appropriate error and cannot be reused", () async { - serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); + test( + "Connection that times out throws appropriate error and cannot be reused", + () async { + serverSocket = + await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2); try { await conn.open(); @@ -462,15 +503,19 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("SSL Connection that times out throws appropriate error and cannot be reused", () async { - serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); + test( + "SSL Connection that times out throws appropriate error and cannot be reused", + () async { + serverSocket = + await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2, useSSL: true); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2, useSSL: true); try { await conn.open(); @@ -480,9 +525,11 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("Connection that times out triggers future for pending queries", () async { + test("Connection that times out triggers future for pending queries", + () async { var openCompleter = new Completer(); - serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); + serverSocket = + await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -492,7 +539,8 @@ void main() { }); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2); conn.open().catchError((e) {}); await openCompleter.future; @@ -505,9 +553,11 @@ void main() { } }); - test("SSL Connection that times out triggers future for pending queries", () async { + test("SSL Connection that times out triggers future for pending queries", + () async { var openCompleter = new Completer(); - serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); + serverSocket = + await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose @@ -517,8 +567,11 @@ void main() { }); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", timeoutInSeconds: 2, useSSL: true); - conn.open().catchError((e) { return null;}); + var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + timeoutInSeconds: 2, useSSL: true); + conn.open().catchError((e) { + return null; + }); await openCompleter.future; @@ -537,7 +590,8 @@ void main() { }); test("If connection is closed, do not allow .execute", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); try { await conn.execute("SELECT 1"); fail('unreachable'); @@ -547,29 +601,32 @@ void main() { }); test("If connection is closed, do not allow .query", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); try { await conn.query("SELECT 1"); fail('unreachable'); } on PostgreSQLException catch (e) { expect(e.toString(), contains("connection is not open")); } - }); test("If connection is closed, do not allow .mappedResultsQuery", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); try { await conn.mappedResultsQuery("SELECT 1"); fail('unreachable'); } on PostgreSQLException catch (e) { expect(e.toString(), contains("connection is not open")); } - }); - test("Queue size, should be 0 on open, >0 if queries added and 0 again after queries executed", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + test( + "Queue size, should be 0 on open, >0 if queries added and 0 again after queries executed", + () async { + final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); expect(conn.queueSize, 0); diff --git a/test/decode_test.dart b/test/decode_test.dart index 2da41c1..7d255fc 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -4,7 +4,8 @@ import 'package:test/test.dart'; void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); await connection.execute(""" @@ -14,19 +15,22 @@ void main() { u uuid) """); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " + await connection.execute( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " "VALUES (-2147483648, -9223372036854775808, TRUE, -32768, " "'string', 10.0, 10.0, '1983-11-06', " "'1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000', " "'{\"key\":\"value\"}', E'\\\\000', '00000000-0000-0000-0000-000000000000')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " + await connection.execute( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " "VALUES (2147483647, 9223372036854775807, FALSE, 32767, " "'a significantly longer string to the point where i doubt this actually matters', " "10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', " "'2183-11-06 00:00:00.999999', " "'[{\"key\":1}]', E'\\\\377', 'FFFFFFFF-ffff-ffff-ffff-ffffffffffff')"); - await connection.execute("INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " + await connection.execute( + "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " "VALUES (null, null, null, null, null, null, null, null, null, null, null, null, null)"); }); tearDown(() async { @@ -66,7 +70,10 @@ void main() { expect(row2[3], equals(2)); expect(row2[4], equals(false)); expect(row2[5], equals(32767)); - expect(row2[6], equals("a significantly longer string to the point where i doubt this actually matters")); + expect( + row2[6], + equals( + "a significantly longer string to the point where i doubt this actually matters")); expect(row2[7] is double, true); expect(row2[7], equals(10.25)); expect(row2[8] is double, true); @@ -82,7 +89,6 @@ void main() { expect(row2[13], equals([255])); expect(row2[14], equals("ffffffff-ffff-ffff-ffff-ffffffffffff")); - // all null row expect(row3[0], isNull); expect(row3[1], equals(3)); @@ -103,8 +109,9 @@ void main() { test("Fetch/insert empty string", () async { await connection.execute("CREATE TEMPORARY TABLE u (t text)"); - var results = - await connection.query("INSERT INTO u (t) VALUES (@t:text) returning t", substitutionValues: {"t": ""}); + var results = await connection.query( + "INSERT INTO u (t) VALUES (@t:text) returning t", + substitutionValues: {"t": ""}); expect(results, [ [""] ]); @@ -117,8 +124,9 @@ void main() { test("Fetch/insert null value", () async { await connection.execute("CREATE TEMPORARY TABLE u (t text)"); - var results = - await connection.query("INSERT INTO u (t) VALUES (@t:text) returning t", substitutionValues: {"t": null}); + var results = await connection.query( + "INSERT INTO u (t) VALUES (@t:text) returning t", + substitutionValues: {"t": null}); expect(results, [ [null] ]); diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 03367cf..d736d12 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -14,7 +14,8 @@ PostgreSQLConnection conn; void main() { group("Binary encoders", () { setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); }); @@ -31,7 +32,8 @@ void main() { await expectInverse(true, PostgreSQLDataType.boolean); await expectInverse(false, PostgreSQLDataType.boolean); try { - await conn.query("INSERT INTO t (v) VALUES (@v:boolean)", substitutionValues: {"v": "not-bool"}); + await conn.query("INSERT INTO t (v) VALUES (@v:boolean)", + substitutionValues: {"v": "not-bool"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: bool")); @@ -43,7 +45,8 @@ void main() { await expectInverse(0, PostgreSQLDataType.smallInteger); await expectInverse(1, PostgreSQLDataType.smallInteger); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int2)", substitutionValues: {"v": "not-int2"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int2)", + substitutionValues: {"v": "not-int2"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); @@ -55,7 +58,8 @@ void main() { await expectInverse(0, PostgreSQLDataType.integer); await expectInverse(1, PostgreSQLDataType.integer); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int4)", substitutionValues: {"v": "not-int4"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int4)", + substitutionValues: {"v": "not-int4"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); @@ -66,12 +70,12 @@ void main() { await expectInverse(0, PostgreSQLDataType.serial); await expectInverse(1, PostgreSQLDataType.serial); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int4)", substitutionValues: {"v": "not-serial"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int4)", + substitutionValues: {"v": "not-serial"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); } - }); test("bigint", () async { @@ -79,24 +83,24 @@ void main() { await expectInverse(0, PostgreSQLDataType.bigInteger); await expectInverse(1, PostgreSQLDataType.bigInteger); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int8)", substitutionValues: {"v": "not-int8"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int8)", + substitutionValues: {"v": "not-int8"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); } - }); test("bigserial", () async { await expectInverse(0, PostgreSQLDataType.bigSerial); await expectInverse(1, PostgreSQLDataType.bigSerial); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int8)", substitutionValues: {"v": "not-bigserial"}); + await conn.query("INSERT INTO t (v) VALUES (@v:int8)", + substitutionValues: {"v": "not-bigserial"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: int")); } - }); test("text", () async { @@ -105,7 +109,8 @@ void main() { await expectInverse("foo\n", PostgreSQLDataType.text); await expectInverse("foo\nbar;s", PostgreSQLDataType.text); try { - await conn.query("INSERT INTO t (v) VALUES (@v:text)", substitutionValues: {"v": 0}); + await conn.query("INSERT INTO t (v) VALUES (@v:text)", + substitutionValues: {"v": 0}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: String")); @@ -117,7 +122,8 @@ void main() { await expectInverse(0.0, PostgreSQLDataType.real); await expectInverse(1.0, PostgreSQLDataType.real); try { - await conn.query("INSERT INTO t (v) VALUES (@v:float4)", substitutionValues: {"v": "not-real"}); + await conn.query("INSERT INTO t (v) VALUES (@v:float4)", + substitutionValues: {"v": "not-real"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: double")); @@ -129,7 +135,8 @@ void main() { await expectInverse(0.0, PostgreSQLDataType.double); await expectInverse(1.0, PostgreSQLDataType.double); try { - await conn.query("INSERT INTO t (v) VALUES (@v:float8)", substitutionValues: {"v": "not-double"}); + await conn.query("INSERT INTO t (v) VALUES (@v:float8)", + substitutionValues: {"v": "not-double"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: double")); @@ -137,11 +144,15 @@ void main() { }); test("date", () async { - await expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLDataType.date); - await expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLDataType.date); - await expectInverse(new DateTime.utc(2016, 10, 1), PostgreSQLDataType.date); + await expectInverse( + new DateTime.utc(1920, 10, 1), PostgreSQLDataType.date); + await expectInverse( + new DateTime.utc(2120, 10, 5), PostgreSQLDataType.date); + await expectInverse( + new DateTime.utc(2016, 10, 1), PostgreSQLDataType.date); try { - await conn.query("INSERT INTO t (v) VALUES (@v:date)", substitutionValues: {"v": "not-date"}); + await conn.query("INSERT INTO t (v) VALUES (@v:date)", + substitutionValues: {"v": "not-date"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: DateTime")); @@ -149,10 +160,13 @@ void main() { }); test("timestamp", () async { - await expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLDataType.timestampWithoutTimezone); - await expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLDataType.timestampWithoutTimezone); + await expectInverse(new DateTime.utc(1920, 10, 1), + PostgreSQLDataType.timestampWithoutTimezone); + await expectInverse(new DateTime.utc(2120, 10, 5), + PostgreSQLDataType.timestampWithoutTimezone); try { - await conn.query("INSERT INTO t (v) VALUES (@v:timestamp)", substitutionValues: {"v": "not-timestamp"}); + await conn.query("INSERT INTO t (v) VALUES (@v:timestamp)", + substitutionValues: {"v": "not-timestamp"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: DateTime")); @@ -160,10 +174,13 @@ void main() { }); test("timestamptz", () async { - await expectInverse(new DateTime.utc(1920, 10, 1), PostgreSQLDataType.timestampWithTimezone); - await expectInverse(new DateTime.utc(2120, 10, 5), PostgreSQLDataType.timestampWithTimezone); + await expectInverse(new DateTime.utc(1920, 10, 1), + PostgreSQLDataType.timestampWithTimezone); + await expectInverse(new DateTime.utc(2120, 10, 5), + PostgreSQLDataType.timestampWithTimezone); try { - await conn.query("INSERT INTO t (v) VALUES (@v:timestamptz)", substitutionValues: {"v": "not-timestamptz"}); + await conn.query("INSERT INTO t (v) VALUES (@v:timestamptz)", + substitutionValues: {"v": "not-timestamptz"}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: DateTime")); @@ -181,18 +198,20 @@ void main() { }, PostgreSQLDataType.json); try { - await conn.query("INSERT INTO t (v) VALUES (@v:jsonb)", substitutionValues: {"v": new DateTime.now()}); + await conn.query("INSERT INTO t (v) VALUES (@v:jsonb)", + substitutionValues: {"v": new DateTime.now()}); fail('unreachable'); } on JsonUnsupportedObjectError catch (_) {} }); test("bytea", () async { await expectInverse([0], PostgreSQLDataType.byteArray); - await expectInverse([1,2,3,4,5], PostgreSQLDataType.byteArray); + await expectInverse([1, 2, 3, 4, 5], PostgreSQLDataType.byteArray); await expectInverse([255, 254, 253], PostgreSQLDataType.byteArray); try { - await conn.query("INSERT INTO t (v) VALUES (@v:bytea)", substitutionValues: {"v": new DateTime.now()}); + await conn.query("INSERT INTO t (v) VALUES (@v:bytea)", + substitutionValues: {"v": new DateTime.now()}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: List")); @@ -200,11 +219,14 @@ void main() { }); test("uuid", () async { - await expectInverse("00000000-0000-0000-0000-000000000000", PostgreSQLDataType.uuid); - await expectInverse("12345678-abcd-efab-cdef-012345678901", PostgreSQLDataType.uuid); + await expectInverse( + "00000000-0000-0000-0000-000000000000", PostgreSQLDataType.uuid); + await expectInverse( + "12345678-abcd-efab-cdef-012345678901", PostgreSQLDataType.uuid); try { - await conn.query("INSERT INTO t (v) VALUES (@v:uuid)", substitutionValues: {"v": new DateTime.now()}); + await conn.query("INSERT INTO t (v) VALUES (@v:uuid)", + substitutionValues: {"v": new DateTime.now()}); fail('unreachable'); } on FormatException catch (e) { expect(e.toString(), contains("Expected: String")); @@ -216,52 +238,63 @@ void main() { test("Escape strings", () { final encoder = new PostgresTextEncoder(true); // ' b o b ' - expect(utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); + expect( + utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); // ' b o \n b ' - expect(utf8.encode(encoder.convert('bo\nb')), equals([39, 98, 111, 10, 98, 39])); + expect(utf8.encode(encoder.convert('bo\nb')), + equals([39, 98, 111, 10, 98, 39])); // ' b o \r b ' - expect(utf8.encode(encoder.convert('bo\rb')), equals([39, 98, 111, 13, 98, 39])); + expect(utf8.encode(encoder.convert('bo\rb')), + equals([39, 98, 111, 13, 98, 39])); // ' b o \b b ' - expect(utf8.encode(encoder.convert('bo\bb')), equals([39, 98, 111, 8, 98, 39])); + expect(utf8.encode(encoder.convert('bo\bb')), + equals([39, 98, 111, 8, 98, 39])); // ' ' ' ' expect(utf8.encode(encoder.convert("'")), equals([39, 39, 39, 39])); // ' ' ' ' ' ' - expect(utf8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); + expect( + utf8.encode(encoder.convert("''")), equals([39, 39, 39, 39, 39, 39])); // ' ' ' ' ' ' - expect(utf8.encode(encoder.convert("\''")), equals([39, 39, 39, 39, 39, 39])); + expect(utf8.encode(encoder.convert("\''")), + equals([39, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' ' ' - expect(utf8.encode(encoder.convert("\\''")), equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); + expect(utf8.encode(encoder.convert("\\''")), + equals([32, 69, 39, 92, 92, 39, 39, 39, 39, 39])); // sp E ' \ \ ' ' ' - expect(utf8.encode(encoder.convert("\\'")), equals([32, 69, 39, 92, 92, 39, 39, 39])); + expect(utf8.encode(encoder.convert("\\'")), + equals([32, 69, 39, 92, 92, 39, 39, 39])); }); test("Encode DateTime", () { // Get users current timezone var tz = new DateTime(2001, 2, 3).timeZoneOffset; var tzOffsetDelimiter = "${tz.isNegative ? '-' : '+'}" - "${tz - .abs() - .inHours - .toString() - .padLeft(2, '0')}" + "${tz.abs().inHours.toString().padLeft(2, '0')}" ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; var pairs = { - "2001-02-03T00:00:00.000$tzOffsetDelimiter": new DateTime(2001, DateTime.february, 3), - "2001-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(2001, DateTime.february, 3, 4, 5, 6, 0), - "2001-02-03T04:05:06.999$tzOffsetDelimiter": new DateTime(2001, DateTime.february, 3, 4, 5, 6, 999), - "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": new DateTime(-10, DateTime.february, 3, 4, 5, 6, 123), - "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-10, DateTime.february, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": new DateTime(-12345, DateTime.february, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter": new DateTime(12345, DateTime.february, 3, 4, 5, 6, 0) + "2001-02-03T00:00:00.000$tzOffsetDelimiter": + new DateTime(2001, DateTime.february, 3), + "2001-02-03T04:05:06.000$tzOffsetDelimiter": + new DateTime(2001, DateTime.february, 3, 4, 5, 6, 0), + "2001-02-03T04:05:06.999$tzOffsetDelimiter": + new DateTime(2001, DateTime.february, 3, 4, 5, 6, 999), + "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": + new DateTime(-10, DateTime.february, 3, 4, 5, 6, 123), + "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": + new DateTime(-10, DateTime.february, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": + new DateTime(-12345, DateTime.february, 3, 4, 5, 6, 0), + "012345-02-03T04:05:06.000$tzOffsetDelimiter": + new DateTime(12345, DateTime.february, 3, 4, 5, 6, 0) }; final encoder = new PostgresTextEncoder(false); @@ -321,7 +354,8 @@ void main() { }); }); - test("UTF8String caches string regardless of which method is called first", () { + test("UTF8String caches string regardless of which method is called first", + () { var u = new UTF8BackedString("abcd"); var v = new UTF8BackedString("abcd"); @@ -368,9 +402,9 @@ Future expectInverse(dynamic value, PostgreSQLDataType dataType) async { final type = PostgreSQLFormat.dataTypeStringForDataType(dataType); await conn.execute("CREATE TEMPORARY TABLE IF NOT EXISTS t (v $type)"); - final result = await conn.query("INSERT INTO t (v) VALUES (${PostgreSQLFormat.id("v", type: dataType)}) RETURNING v", substitutionValues: { - "v": value - }); + final result = await conn.query( + "INSERT INTO t (v) VALUES (${PostgreSQLFormat.id("v", type: dataType)}) RETURNING v", + substitutionValues: {"v": value}); expect(result.first.first, equals(value)); final encoder = new PostgresBinaryEncoder(dataType); diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 6203950..810a528 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -4,22 +4,30 @@ import 'package:postgres/postgres.dart'; import 'package:postgres/src/query.dart'; import 'package:test/test.dart'; - void main() { test("Ensure all types/format type mappings are available and accurate", () { - PostgreSQLDataType.values.where((t) => t != PostgreSQLDataType.bigSerial && t != PostgreSQLDataType.serial).forEach((t) { - expect(PostgreSQLFormatIdentifier.typeStringToCodeMap.values.contains(t), true); - final code = PostgreSQLFormat.dataTypeStringForDataType(t); + PostgreSQLDataType.values + .where((t) => + t != PostgreSQLDataType.bigSerial && t != PostgreSQLDataType.serial) + .forEach((t) { + expect(PostgreSQLFormatIdentifier.typeStringToCodeMap.values.contains(t), + true); + final code = PostgreSQLFormat.dataTypeStringForDataType(t); expect(PostgreSQLFormatIdentifier.typeStringToCodeMap[code], t); }); }); test("Ensure bigserial gets translated to int8", () { - expect(PostgreSQLFormat.dataTypeStringForDataType(PostgreSQLDataType.serial), "int4"); + expect( + PostgreSQLFormat.dataTypeStringForDataType(PostgreSQLDataType.serial), + "int4"); }); test("Ensure serial gets translated to int4", () { - expect(PostgreSQLFormat.dataTypeStringForDataType(PostgreSQLDataType.bigSerial), "int8"); + expect( + PostgreSQLFormat.dataTypeStringForDataType( + PostgreSQLDataType.bigSerial), + "int8"); }); test("Simple replacement", () { @@ -48,56 +56,52 @@ void main() { }); test("Identifiers next to eachother with type info", () { - var result = PostgreSQLFormat - .substitute("@id:int2@foo:float4", {"id": 12, "foo": 2.0}); + var result = PostgreSQLFormat.substitute( + "@id:int2@foo:float4", {"id": 12, "foo": 2.0}); expect(result, equals("122.0")); }); test("Disambiguate PostgreSQL typecast", () { - var result = PostgreSQLFormat - .substitute("@id::jsonb", {"id": "12"}); + var result = PostgreSQLFormat.substitute("@id::jsonb", {"id": "12"}); expect(result, "'12'::jsonb"); }); test("PostgreSQL typecast appears in query", () { - var results = PostgreSQLFormat.substitute("SELECT * FROM t WHERE id=@id:int2 WHERE blob=@blob::jsonb AND blob='{\"a\":1}'::jsonb", { - "id": 2, - "blob": "{\"key\":\"value\"}" - }); + var results = PostgreSQLFormat.substitute( + "SELECT * FROM t WHERE id=@id:int2 WHERE blob=@blob::jsonb AND blob='{\"a\":1}'::jsonb", + {"id": 2, "blob": "{\"key\":\"value\"}"}); - expect(results, "SELECT * FROM t WHERE id=2 WHERE blob='{\"key\":\"value\"}'::jsonb AND blob='{\"a\":1}'::jsonb"); + expect(results, + "SELECT * FROM t WHERE id=2 WHERE blob='{\"key\":\"value\"}'::jsonb AND blob='{\"a\":1}'::jsonb"); }); test("Can both provide type and typecast", () { - var results = PostgreSQLFormat.substitute("SELECT * FROM t WHERE id=@id:int2::int4", { - "id": 2, - "blob": "{\"key\":\"value\"}" - }); + var results = PostgreSQLFormat.substitute( + "SELECT * FROM t WHERE id=@id:int2::int4", + {"id": 2, "blob": "{\"key\":\"value\"}"}); expect(results, "SELECT * FROM t WHERE id=2::int4"); }); test("UTF16 symbols with quotes", () { var value = "'©™®'"; - var results = PostgreSQLFormat.substitute("INSERT INTO t (t) VALUES (@t)", { - "t": value - }); + var results = PostgreSQLFormat.substitute( + "INSERT INTO t (t) VALUES (@t)", {"t": value}); expect(results, "INSERT INTO t (t) VALUES ('''©™®''')"); }); test("UTF16 symbols with backslash", () { var value = "'©\\™®'"; - var results = PostgreSQLFormat.substitute("INSERT INTO t (t) VALUES (@t)", { - "t": value - }); + var results = PostgreSQLFormat.substitute( + "INSERT INTO t (t) VALUES (@t)", {"t": value}); expect(results, "INSERT INTO t (t) VALUES ( E'''©\\\\™®''')"); }); test("String identifiers get escaped", () { - var result = PostgreSQLFormat - .substitute("@id:text @foo", {"id": "1';select", "foo": "3\\4"}); + var result = PostgreSQLFormat.substitute( + "@id:text @foo", {"id": "1';select", "foo": "3\\4"}); // ' 1 ' ' ; s e l e c t ' sp sp E ' 3 \ \ 4 ' expect(utf8.encode(result), [ diff --git a/test/json_test.dart b/test/json_test.dart index eaa7587..8a95e1d 100644 --- a/test/json_test.dart +++ b/test/json_test.dart @@ -5,7 +5,8 @@ void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); await connection.execute(""" @@ -19,67 +20,133 @@ void main() { group("Storage", () { test("Can store JSON String", () async { - var result = await connection.query("INSERT INTO t (j) VALUES ('\"xyz\"'::jsonb) RETURNING j"); - expect(result, [["xyz"]]); + var result = await connection + .query("INSERT INTO t (j) VALUES ('\"xyz\"'::jsonb) RETURNING j"); + expect(result, [ + ["xyz"] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [["xyz"]]); + expect(result, [ + ["xyz"] + ]); }); test("Can store JSON String with driver type annotation", () async { - var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { - "a" : "xyz" - }); - expect(result, [["xyz"]]); + var result = await connection.query( + "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", + substitutionValues: {"a": "xyz"}); + expect(result, [ + ["xyz"] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [["xyz"]]); + expect(result, [ + ["xyz"] + ]); }); test("Can store JSON Number", () async { - var result = await connection.query("INSERT INTO t (j) VALUES ('4'::jsonb) RETURNING j"); - expect(result, [[4]]); + var result = await connection + .query("INSERT INTO t (j) VALUES ('4'::jsonb) RETURNING j"); + expect(result, [ + [4] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [[4]]); + expect(result, [ + [4] + ]); }); test("Can store JSON Number with driver type annotation", () async { - var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { - "a": 4 - }); - expect(result, [[4]]); + var result = await connection.query( + "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", + substitutionValues: {"a": 4}); + expect(result, [ + [4] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [[4]]); + expect(result, [ + [4] + ]); }); test("Can store JSON map", () async { - var result = await connection.query("INSERT INTO t (j) VALUES ('{\"a\":4}') RETURNING j"); - expect(result, [[{"a":4}]]); + var result = await connection + .query("INSERT INTO t (j) VALUES ('{\"a\":4}') RETURNING j"); + expect(result, [ + [ + {"a": 4} + ] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [[{"a":4}]]); + expect(result, [ + [ + {"a": 4} + ] + ]); }); test("Can store JSON map with driver type annotation", () async { - var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { - "a": {"a":4} - }); - expect(result, [[{"a":4}]]); + var result = await connection.query( + "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", + substitutionValues: { + "a": {"a": 4} + }); + expect(result, [ + [ + {"a": 4} + ] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [[{"a":4}]]); + expect(result, [ + [ + {"a": 4} + ] + ]); }); test("Can store JSON list", () async { - var result = await connection.query("INSERT INTO t (j) VALUES ('[{\"a\":4}]') RETURNING j"); - expect(result, [[[{"a":4}]]]); + var result = await connection + .query("INSERT INTO t (j) VALUES ('[{\"a\":4}]') RETURNING j"); + expect(result, [ + [ + [ + {"a": 4} + ] + ] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [[[{"a":4}]]]); + expect(result, [ + [ + [ + {"a": 4} + ] + ] + ]); }); test("Can store JSON list with driver type annotation", () async { - var result = await connection.query("INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", substitutionValues: { - "a": [{"a":4}] - }); - expect(result, [[[{"a":4}]]]); + var result = await connection.query( + "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", + substitutionValues: { + "a": [ + {"a": 4} + ] + }); + expect(result, [ + [ + [ + {"a": 4} + ] + ] + ]); result = await connection.query("SELECT j FROM t"); - expect(result, [[[{"a":4}]]]); + expect(result, [ + [ + [ + {"a": 4} + ] + ] + ]); }); }); } diff --git a/test/map_return_test.dart b/test/map_return_test.dart index ad02f9b..2a1de3b 100644 --- a/test/map_return_test.dart +++ b/test/map_return_test.dart @@ -6,7 +6,8 @@ void main() { InterceptingConnection connection; setUp(() async { - connection = new InterceptingConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + connection = new InterceptingConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await connection.open(); await connection.execute(""" @@ -20,9 +21,12 @@ void main() { await connection.execute("INSERT INTO t (id, name) VALUES (1, 'a')"); await connection.execute("INSERT INTO t (id, name) VALUES (2, 'b')"); await connection.execute("INSERT INTO t (id, name) VALUES (3, 'c')"); - await connection.execute("INSERT INTO u (id, name, t_id) VALUES (1, 'ua', 1)"); - await connection.execute("INSERT INTO u (id, name, t_id) VALUES (2, 'ub', 1)"); - await connection.execute("INSERT INTO u (id, name, t_id) VALUES (3, 'uc', 2)"); + await connection + .execute("INSERT INTO u (id, name, t_id) VALUES (1, 'ua', 1)"); + await connection + .execute("INSERT INTO u (id, name, t_id) VALUES (2, 'ub', 1)"); + await connection + .execute("INSERT INTO u (id, name, t_id) VALUES (3, 'uc', 2)"); }); tearDown(() async { @@ -30,7 +34,8 @@ void main() { }); test("Get row map without specifying columns", () async { - final results = await connection.mappedResultsQuery("SELECT * from t ORDER BY id ASC"); + final results = + await connection.mappedResultsQuery("SELECT * from t ORDER BY id ASC"); expect(results, [ { "t": {"id": 1, "name": "a"} @@ -45,7 +50,8 @@ void main() { }); test("Get row map by with specified columns", () async { - final results = await connection.mappedResultsQuery("SELECT name, id from t ORDER BY id ASC"); + final results = await connection + .mappedResultsQuery("SELECT name, id from t ORDER BY id ASC"); expect(results, [ { "t": {"id": 1, "name": "a"} @@ -58,7 +64,8 @@ void main() { }, ]); - final nextResults = await connection.mappedResultsQuery("SELECT name from t ORDER BY name DESC"); + final nextResults = await connection + .mappedResultsQuery("SELECT name from t ORDER BY name DESC"); expect(nextResults, [ { "t": {"name": "c"} @@ -96,7 +103,8 @@ void main() { }); test("Table names get cached", () async { - final regex = new RegExp("SELECT relname FROM pg_class WHERE relkind='r' AND oid IN \\(([0-9]*)\\) ORDER BY oid ASC"); + final regex = new RegExp( + "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN \\(([0-9]*)\\) ORDER BY oid ASC"); final oids = []; await connection.mappedResultsQuery("SELECT id FROM t"); @@ -108,7 +116,8 @@ void main() { await connection.mappedResultsQuery("SELECT id FROM t"); expect(connection.queries.length, 0); - await connection.mappedResultsQuery("SELECT t.id, u.id FROM t LEFT OUTER JOIN u ON t.id=u.t_id"); + await connection.mappedResultsQuery( + "SELECT t.id, u.id FROM t LEFT OUTER JOIN u ON t.id=u.t_id"); expect(connection.queries.length, 1); match = regex.firstMatch(connection.queries.first); expect(oids.contains(match.group(1)), false); @@ -121,20 +130,28 @@ void main() { test("Non-table mappedResultsQuery succeeds", () async { final result = await connection.mappedResultsQuery("SELECT 1"); - expect(result, [{null: {"?column?": 1}}]); + expect(result, [ + { + null: {"?column?": 1} + } + ]); }); } class InterceptingConnection extends PostgreSQLConnection { - InterceptingConnection(String host, int port, String databaseName, {String username: null, String password: null}) + InterceptingConnection(String host, int port, String databaseName, + {String username: null, String password: null}) : super(host, port, databaseName, username: username, password: password); List queries = []; @override Future>> query(String fmtString, - {Map substitutionValues: null, bool allowReuse: true, int timeoutInSeconds}) { + {Map substitutionValues: null, + bool allowReuse: true, + int timeoutInSeconds}) { queries.add(fmtString); - return super.query(fmtString, substitutionValues: substitutionValues, allowReuse: allowReuse); + return super.query(fmtString, + substitutionValues: substitutionValues, allowReuse: allowReuse); } } diff --git a/test/notification_test.dart b/test/notification_test.dart index 1aa6017..fbe32e7 100644 --- a/test/notification_test.dart +++ b/test/notification_test.dart @@ -22,12 +22,10 @@ void main() { var channel = 'virtual'; var payload = 'This is the payload'; var futureMsg = connection.notifications.first; - await connection - .execute("LISTEN $channel;" - "NOTIFY $channel, '$payload';"); + await connection.execute("LISTEN $channel;" + "NOTIFY $channel, '$payload';"); - var msg = await futureMsg - .timeout(new Duration(milliseconds: 200)); + var msg = await futureMsg.timeout(new Duration(milliseconds: 200)); expect(msg.channel, channel); expect(msg.payload, payload); }); @@ -35,12 +33,10 @@ void main() { test("Notification Response empty payload", () async { var channel = 'virtual'; var futureMsg = connection.notifications.first; - await connection - .execute("LISTEN $channel;" - "NOTIFY $channel;"); + await connection.execute("LISTEN $channel;" + "NOTIFY $channel;"); - var msg = await futureMsg - .timeout(new Duration(milliseconds: 200)); + var msg = await futureMsg.timeout(new Duration(milliseconds: 200)); expect(msg.channel, channel); expect(msg.payload, ''); }); @@ -49,27 +45,22 @@ void main() { var channel = 'virtual'; var payload = 'This is the payload'; var futureMsg = connection.notifications.first; - await connection - .execute("LISTEN $channel;" + await connection.execute("LISTEN $channel;" "NOTIFY $channel, '$payload';"); - var msg = await futureMsg - .timeout(new Duration(milliseconds: 200)); + var msg = await futureMsg.timeout(new Duration(milliseconds: 200)); expect(msg.channel, channel); expect(msg.payload, payload); - await connection - .execute("UNLISTEN $channel;"); + await connection.execute("UNLISTEN $channel;"); futureMsg = connection.notifications.first; try { - await connection - .execute("NOTIFY $channel, '$payload';"); + await connection.execute("NOTIFY $channel, '$payload';"); - await futureMsg - .timeout(new Duration(milliseconds: 200)); + await futureMsg.timeout(new Duration(milliseconds: 200)); fail('There should be no notification'); } on TimeoutException catch (_) {} @@ -79,12 +70,11 @@ void main() { Map countResponse = new Map(); int totalCountResponse = 0; Completer finishExecute = new Completer(); - connection.notifications.listen((msg){ + connection.notifications.listen((msg) { int count = countResponse[msg.channel]; countResponse[msg.channel] = (count ?? 0) + 1; totalCountResponse++; - if(totalCountResponse == 20) - finishExecute.complete(); + if (totalCountResponse == 20) finishExecute.complete(); }); var channel1 = 'virtual1'; @@ -92,30 +82,24 @@ void main() { var notifier = () async { for (int i = 0; i < 5; i++) { - await connection - .execute("NOTIFY $channel1;" + await connection.execute("NOTIFY $channel1;" "NOTIFY $channel2;"); } }; - await connection - .execute("LISTEN $channel1;"); + await connection.execute("LISTEN $channel1;"); await notifier(); - await connection - .execute("LISTEN $channel2;"); + await connection.execute("LISTEN $channel2;"); await notifier(); - await connection - .execute("UNLISTEN $channel1;"); + await connection.execute("UNLISTEN $channel1;"); await notifier(); - await connection - .execute("UNLISTEN $channel2;"); + await connection.execute("UNLISTEN $channel2;"); await notifier(); - await finishExecute.future - .timeout(new Duration(milliseconds: 200)); + await finishExecute.future.timeout(new Duration(milliseconds: 200)); expect(countResponse[channel1], 10); expect(countResponse[channel2], 10); diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index 8469592..77127ae 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -561,8 +561,10 @@ void main() { Map cachedQueryMap(PostgreSQLConnection connection) { var cacheMirror = reflect(connection).type.declarations.values.firstWhere( (DeclarationMirror dm) => dm.simpleName.toString().contains("_cache")); - return reflect(connection).getField(cacheMirror.simpleName).getField(#queries).reflectee - as Map; + return reflect(connection) + .getField(cacheMirror.simpleName) + .getField(#queries) + .reflectee as Map; } bool hasCachedQueryNamed(PostgreSQLConnection connection, String name) { diff --git a/test/query_test.dart b/test/query_test.dart index 4c2e883..cdc87f0 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -11,12 +11,11 @@ void main() { connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); await connection.open(); - await connection.execute( - "CREATE TEMPORARY TABLE t " - "(i int, s serial, bi bigint, " - "bs bigserial, bl boolean, si smallint, " - "t text, f real, d double precision, " - "dt date, ts timestamp, tsz timestamptz, j jsonb, u uuid)"); + await connection.execute("CREATE TEMPORARY TABLE t " + "(i int, s serial, bi bigint, " + "bs bigserial, bl boolean, si smallint, " + "t text, f real, d double precision, " + "dt date, ts timestamp, tsz timestamptz, j jsonb, u uuid)"); await connection.execute( "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); await connection @@ -57,7 +56,7 @@ void main() { test("UTF16 strings in value with escape characters", () async { await connection.execute( "INSERT INTO t (t) values " - "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", + "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", substitutionValues: { "t": "'©™®'", }); @@ -71,7 +70,7 @@ void main() { test("UTF16 strings in value with backslash", () async { await connection.execute( "INSERT INTO t (t) values " - "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", + "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", substitutionValues: { "t": "°\\'©™®'", }); @@ -133,7 +132,7 @@ void main() { "dt": new DateTime.utc(2000), "ts": new DateTime.utc(2000, 2), "tsz": new DateTime.utc(2000, 3), - "j": {"a":"b"}, + "j": {"a": "b"}, "u": "01234567-89ab-cdef-0123-0123456789ab" }); @@ -150,12 +149,12 @@ void main() { new DateTime.utc(2000), new DateTime.utc(2000, 2), new DateTime.utc(2000, 3), - {"a":"b"}, + {"a": "b"}, "01234567-89ab-cdef-0123-0123456789ab" ]; expect(result, [expectedRow]); - result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); + result = await connection.query( + "select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); expect(result, [expectedRow]); }); @@ -174,7 +173,7 @@ void main() { "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}," "${PostgreSQLFormat.id("j", type: PostgreSQLDataType.json)}," "${PostgreSQLFormat.id("u", type: PostgreSQLDataType.uuid)})" - " returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u", + " returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u", substitutionValues: { "i": 1, "bi": 2, @@ -208,8 +207,8 @@ void main() { ]; expect(result, [expectedRow]); - result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); + result = await connection.query( + "select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); expect(result, [expectedRow]); }); @@ -315,11 +314,8 @@ void main() { test("Can cast text to int on db server", () async { var results = await connection.query( - "INSERT INTO u (i1, i2) VALUES (@i1::int4, @i2::int4) RETURNING i1, i2", - substitutionValues: { - "i1": "0", - "i2": "1" - }); + "INSERT INTO u (i1, i2) VALUES (@i1::int4, @i2::int4) RETURNING i1, i2", + substitutionValues: {"i1": "0", "i2": "1"}); expect(results, [ [0, 1] @@ -327,7 +323,6 @@ void main() { }); }); - group("Unsuccesful queries", () { var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); @@ -382,10 +377,7 @@ void main() { try { await connection.query( "INSERT INTO t (i1, i2) values (@i1:int4, @i2:int4)", - substitutionValues: { - "i1": "1", - "i2": 1 - }); + substitutionValues: {"i1": "1", "i2": 1}); expect(true, false); } on FormatException catch (e) { expect(e.toString(), contains("Invalid type for parameter value")); @@ -396,10 +388,7 @@ void main() { try { await connection.query( "INSERT INTO t (i1, i2) values (@i1:qwerty, @i2:int4)", - substitutionValues: { - "i1": "1", - "i2": 1 - }); + substitutionValues: {"i1": "1", "i2": 1}); expect(true, false); } on FormatException catch (e) { expect(e.toString(), contains("Invalid type code")); diff --git a/test/timeout_test.dart b/test/timeout_test.dart index 2d2b40e..1d94f9e 100644 --- a/test/timeout_test.dart +++ b/test/timeout_test.dart @@ -6,7 +6,8 @@ void main() { PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -15,7 +16,9 @@ void main() { await conn?.close(); }); - test("Timeout fires on query while in queue does not execute query, query throws exception", () async { + test( + "Timeout fires on query while in queue does not execute query, query throws exception", + () async { //ignore: unawaited_futures final f = conn.query("SELECT pg_sleep(2)"); try { @@ -38,7 +41,9 @@ void main() { expect(await conn.query("SELECT * from t"), hasLength(0)); }); - test("Query on parent context for transaction completes (with error) after timeout", () async { + test( + "Query on parent context for transaction completes (with error) after timeout", + () async { try { await conn.transaction((ctx) async { await conn.query("SELECT 1", timeoutInSeconds: 1); @@ -50,7 +55,9 @@ void main() { expect(await conn.query("SELECT * from t"), hasLength(0)); }); - test("If query is already on the wire and times out, safely throws timeoutexception and nothing else", () async { + test( + "If query is already on the wire and times out, safely throws timeoutexception and nothing else", + () async { try { await conn.query("SELECT pg_sleep(2)", timeoutInSeconds: 1); fail('unreachable'); @@ -59,9 +66,13 @@ void main() { test("Query times out, next query in the queue runs", () async { //ignore: unawaited_futures - conn.query("SELECT pg_sleep(2)", timeoutInSeconds: 1).catchError((_) => null); + conn + .query("SELECT pg_sleep(2)", timeoutInSeconds: 1) + .catchError((_) => null); - expect(await conn.query("SELECT 1"), [[1]]); + expect(await conn.query("SELECT 1"), [ + [1] + ]); }); test("Query that succeeds does not timeout", () async { @@ -70,7 +81,9 @@ void main() { }); test("Query that fails does not timeout", () async { - await conn.query("INSERT INTO t (id) VALUES ('foo')", timeoutInSeconds: 1).catchError((_) => null); + await conn + .query("INSERT INTO t (id) VALUES ('foo')", timeoutInSeconds: 1) + .catchError((_) => null); expect(new Future.delayed(new Duration(seconds: 2)), completes); }); } diff --git a/test/transaction_test.dart b/test/transaction_test.dart index b9d8a8e..8cc8785 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -9,7 +9,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -22,7 +23,8 @@ void main() { await conn.execute("INSERT INTO t (id) VALUES (1)"); final List> outValue = await conn.transaction((ctx) async { - return await ctx.query('SELECT * FROM t WHERE id = @id LIMIT 1', substitutionValues: {'id': 1}); + return await ctx.query('SELECT * FROM t WHERE id = @id LIMIT 1', + substitutionValues: {'id': 1}); }); expect(outValue.length, 1); @@ -31,7 +33,8 @@ void main() { expect(outValue.first.first, 1); }); - test("Send successful transaction succeeds, returns returned value", () async { + test("Send successful transaction succeeds, returns returned value", + () async { var outResult = await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); @@ -47,7 +50,8 @@ void main() { ]); }); - test("Query during transaction must wait until transaction is finished", () async { + test("Query during transaction must wait until transaction is finished", + () async { var orderEnsurer = []; var nextCompleter = new Completer.sync(); var outResult = conn.transaction((c) async { @@ -80,7 +84,8 @@ void main() { ]); }); - test("Make sure two simultaneous transactions cannot be interwoven", () async { + test("Make sure two simultaneous transactions cannot be interwoven", + () async { var orderEnsurer = []; var firstTransactionFuture = conn.transaction((c) async { @@ -138,7 +143,8 @@ void main() { expect(result, []); }); - test("Intentional rollback from outside of a transaction has no impact", () async { + test("Intentional rollback from outside of a transaction has no impact", + () async { var orderEnsurer = []; var nextCompleter = new Completer.sync(); var outResult = conn.transaction((c) async { @@ -229,12 +235,15 @@ void main() { }).catchError((e) => transactionError = e); expect(transactionError, isNotNull); expect(failingQueryError.toString(), contains("invalid input")); - expect(pendingQueryError.toString(), contains("failed prior to execution")); + expect( + pendingQueryError.toString(), contains("failed prior to execution")); var total = await conn.query("SELECT id FROM t"); expect(total, []); }); - test("A transaction with a rollback and non-await queries rolls back transaction", () async { + test( + "A transaction with a rollback and non-await queries rolls back transaction", + () async { var errs = []; await conn.transaction((ctx) async { ctx.query("INSERT INTO t (id) VALUES (1)").catchError((e) { @@ -253,7 +262,8 @@ void main() { expect(errs.length, 2); }); - test("A transaction that mixes awaiting and non-awaiting queries fails gracefully when an awaited query fails", + test( + "A transaction that mixes awaiting and non-awaiting queries fails gracefully when an awaited query fails", () async { var transactionError; await conn.transaction((ctx) async { @@ -267,7 +277,8 @@ void main() { expect(total, []); }); - test("A transaction that mixes awaiting and non-awaiting queries fails gracefully when an unawaited query fails", + test( + "A transaction that mixes awaiting and non-awaiting queries fails gracefully when an unawaited query fails", () async { var transactionError; await conn.transaction((ctx) async { @@ -289,7 +300,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -387,7 +399,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); @@ -462,12 +475,15 @@ void main() { expect(result, []); }); - test("If exception thrown while preparing query, transaction gets rolled back", () async { + test( + "If exception thrown while preparing query, transaction gets rolled back", + () async { try { await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); - c.query("INSERT INTO t (id) VALUES (@id:int4)", substitutionValues: {"id": "foobar"}).catchError((_) => null); + c.query("INSERT INTO t (id) VALUES (@id:int4)", + substitutionValues: {"id": "foobar"}).catchError((_) => null); await c.query("INSERT INTO t (id) VALUES (2)"); }); expect(true, false); @@ -498,11 +514,15 @@ void main() { expect(res, []); }); - test("When exception thrown in unawaited on future, transaction is rolled back", () async { + test( + "When exception thrown in unawaited on future, transaction is rolled back", + () async { try { await conn.transaction((c) async { await c.query("INSERT INTO t (id) VALUES (1)"); - c.query("INSERT INTO t (id) VALUE ('foo') RETURNING id").catchError((_) => null); + c + .query("INSERT INTO t (id) VALUE ('foo') RETURNING id") + .catchError((_) => null); await c.query("INSERT INTO t (id) VALUES (2)"); }); fail('unreachable'); @@ -517,7 +537,8 @@ void main() { PostgreSQLConnection conn = null; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart"); + conn = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); await conn.open(); await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); }); From 1e25f901b512699d533908a71705e276447ef9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 21 Mar 2019 00:47:16 +0100 Subject: [PATCH 45/73] Upgrade sources: Dart 2.2 format, pedantic + other strict linter rules. (#81) Upgrade sources: Dart 2.2 format, pedantic + other strict linter rules. --- analysis_options.yaml | 64 ++++- lib/postgres.dart | 4 +- lib/src/binary_codec.dart | 181 ++++++------- lib/src/client_messages.dart | 68 +++-- lib/src/connection.dart | 180 ++++++------- lib/src/connection_fsm.dart | 85 +++--- lib/src/exceptions.dart | 24 +- lib/src/execution_context.dart | 17 +- lib/src/message_window.dart | 40 +-- lib/src/query.dart | 140 +++++----- lib/src/query_cache.dart | 8 +- lib/src/query_queue.dart | 9 +- lib/src/server_messages.dart | 104 +++++--- lib/src/substituter.dart | 106 ++++---- lib/src/text_codec.dart | 52 ++-- lib/src/transaction_proxy.dart | 31 ++- lib/src/utf8_backed_string.dart | 8 +- pubspec.yaml | 5 +- test/connection_test.dart | 411 +++++++++++++++-------------- test/decode_test.dart | 78 +++--- test/encoding_test.dart | 303 +++++++++++---------- test/framer_test.dart | 165 ++++++------ test/interpolation_test.dart | 93 +++---- test/json_test.dart | 84 +++--- test/map_return_test.dart | 84 +++--- test/notification_test.dart | 78 +++--- test/query_reuse_test.dart | 449 ++++++++++++++++---------------- test/query_test.dart | 380 +++++++++++++-------------- test/timeout_test.dart | 54 ++-- test/transaction_test.dart | 380 ++++++++++++++------------- 30 files changed, 1887 insertions(+), 1798 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 2c63e94..ab0596b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,19 +3,61 @@ # The commented part below is just for inspiration. Read the guide here: # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer +include: package:pedantic/analysis_options.yaml + analyzer: -# strong-mode: -# implicit-casts: false -# excludes: -# - path/to/excluded/files/** + errors: + override_on_non_overriding_method: error + unused_element: error + unused_import: error + unused_local_variable: error + dead_code: error + strong-mode: + implicit-casts: false linter: rules: # see catalogue here: http://dart-lang.github.io/linter/lints/ - - camel_case_types - - hash_and_equals - - iterable_contains_unrelated_type - - list_remove_unrelated_type - - unawaited_futures - - unrelated_type_equality_checks - - valid_regexps + - annotate_overrides + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - directives_ordering +# - empty_catches + - empty_statements + - hash_and_equals + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + - only_throw_errors + - overridden_fields + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_contains + - prefer_final_fields + - prefer_final_locals + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - valid_regexps diff --git a/lib/postgres.dart b/lib/postgres.dart index 050dc89..01c9235 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -1,6 +1,6 @@ library postgres; export 'src/connection.dart'; -export 'src/types.dart'; -export 'src/substituter.dart'; export 'src/execution_context.dart'; +export 'src/substituter.dart'; +export 'src/types.dart'; diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index 1b061f5..2108663 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -1,9 +1,10 @@ import 'dart:convert'; - import 'dart:typed_data'; -import 'package:postgres/postgres.dart'; -import 'package:postgres/src/types.dart'; +import 'package:buffer/buffer.dart'; + +import '../postgres.dart'; +import 'types.dart'; class PostgresBinaryEncoder extends Converter { const PostgresBinaryEncoder(this.dataType); @@ -19,125 +20,114 @@ class PostgresBinaryEncoder extends Converter { switch (dataType) { case PostgreSQLDataType.boolean: { - if (value is! bool) { - throw new FormatException( - "Invalid type for parameter value. Expected: bool Got: ${value.runtimeType}"); + if (value is bool) { + final bd = ByteData(1); + bd.setUint8(0, value ? 1 : 0); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(1); - bd.setUint8(0, value ? 1 : 0); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: bool Got: ${value.runtimeType}'); } case PostgreSQLDataType.bigSerial: case PostgreSQLDataType.bigInteger: { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); + if (value is int) { + final bd = ByteData(8); + bd.setInt64(0, value); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(8); - bd.setInt64(0, value); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: int Got: ${value.runtimeType}'); } case PostgreSQLDataType.serial: case PostgreSQLDataType.integer: { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); + if (value is int) { + final bd = ByteData(4); + bd.setInt32(0, value); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(4); - bd.setInt32(0, value); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: int Got: ${value.runtimeType}'); } case PostgreSQLDataType.smallInteger: { - if (value is! int) { - throw new FormatException( - "Invalid type for parameter value. Expected: int Got: ${value.runtimeType}"); + if (value is int) { + final bd = ByteData(2); + bd.setInt16(0, value); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(2); - bd.setInt16(0, value); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: int Got: ${value.runtimeType}'); } case PostgreSQLDataType.name: case PostgreSQLDataType.text: { - if (value is! String) { - throw new FormatException( - "Invalid type for parameter value. Expected: String Got: ${value.runtimeType}"); + if (value is String) { + return castBytes(utf8.encode(value)); } - - return utf8.encode(value); + throw FormatException( + 'Invalid type for parameter value. Expected: String Got: ${value.runtimeType}'); } case PostgreSQLDataType.real: { - if (value is! double) { - throw new FormatException( - "Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); + if (value is double) { + final bd = ByteData(4); + bd.setFloat32(0, value); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(4); - bd.setFloat32(0, value); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: double Got: ${value.runtimeType}'); } case PostgreSQLDataType.double: { - if (value is! double) { - throw new FormatException( - "Invalid type for parameter value. Expected: double Got: ${value.runtimeType}"); + if (value is double) { + final bd = ByteData(8); + bd.setFloat64(0, value); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(8); - bd.setFloat64(0, value); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: double Got: ${value.runtimeType}'); } case PostgreSQLDataType.date: { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); + if (value is DateTime) { + final bd = ByteData(4); + bd.setInt32(0, value.toUtc().difference(DateTime.utc(2000)).inDays); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(4); - bd.setInt32( - 0, value.toUtc().difference(new DateTime.utc(2000)).inDays); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}'); } case PostgreSQLDataType.timestampWithoutTimezone: { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); + if (value is DateTime) { + final bd = ByteData(8); + final diff = value.toUtc().difference(DateTime.utc(2000)); + bd.setInt64(0, diff.inMicroseconds); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(8); - var diff = value.toUtc().difference(new DateTime.utc(2000)); - bd.setInt64(0, diff.inMicroseconds); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}'); } case PostgreSQLDataType.timestampWithTimezone: { - if (value is! DateTime) { - throw new FormatException( - "Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}"); + if (value is DateTime) { + final bd = ByteData(8); + bd.setInt64( + 0, value.toUtc().difference(DateTime.utc(2000)).inMicroseconds); + return bd.buffer.asUint8List(); } - - var bd = new ByteData(8); - bd.setInt64(0, - value.toUtc().difference(new DateTime.utc(2000)).inMicroseconds); - return bd.buffer.asUint8List(); + throw FormatException( + 'Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}'); } case PostgreSQLDataType.json: { - var jsonBytes = utf8.encode(json.encode(value)); - final outBuffer = new Uint8List(jsonBytes.length + 1); + final jsonBytes = utf8.encode(json.encode(value)); + final outBuffer = Uint8List(jsonBytes.length + 1); outBuffer[0] = 1; for (var i = 0; i < jsonBytes.length; i++) { outBuffer[i + 1] = jsonBytes[i]; @@ -148,28 +138,28 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.byteArray: { - if (value is! List) { - throw new FormatException( - "Invalid type for parameter value. Expected: List Got: ${value.runtimeType}"); + if (value is List) { + return castBytes(value); } - return new Uint8List.fromList(value); + throw FormatException( + 'Invalid type for parameter value. Expected: List Got: ${value.runtimeType}'); } case PostgreSQLDataType.uuid: { if (value is! String) { - throw new FormatException( - "Invalid type for parameter value. Expected: String Got: ${value.runtimeType}"); + throw FormatException( + 'Invalid type for parameter value. Expected: String Got: ${value.runtimeType}'); } - final dashUnit = "-".codeUnits.first; + final dashUnit = '-'.codeUnits.first; final hexBytes = (value as String) .toLowerCase() .codeUnits .where((c) => c != dashUnit) .toList(); if (hexBytes.length != 32) { - throw new FormatException( + throw FormatException( "Invalid UUID string. There must be exactly 32 hexadecimal (0-9 and a-f) characters and any number of '-' characters."); } @@ -180,11 +170,11 @@ class PostgresBinaryEncoder extends Converter { return charCode - 87; } - throw new FormatException( - "Invalid UUID string. Contains non-hexadecimal character (0-9 and a-f)."); + throw FormatException( + 'Invalid UUID string. Contains non-hexadecimal character (0-9 and a-f).'); }; - final outBuffer = new Uint8List(16); + final outBuffer = Uint8List(16); for (var i = 0; i < hexBytes.length; i += 2) { final upperByte = byteConvert(hexBytes[i]); final lowerByte = byteConvert(hexBytes[i + 1]); @@ -195,7 +185,7 @@ class PostgresBinaryEncoder extends Converter { } } - throw new PostgreSQLException("Unsupported datatype"); + throw PostgreSQLException('Unsupported datatype'); } } @@ -212,8 +202,8 @@ class PostgresBinaryDecoder extends Converter { return null; } - final buffer = new ByteData.view( - value.buffer, value.offsetInBytes, value.lengthInBytes); + final buffer = + ByteData.view(value.buffer, value.offsetInBytes, value.lengthInBytes); switch (dataType) { case PostgreSQLDataType.name: @@ -236,12 +226,11 @@ class PostgresBinaryDecoder extends Converter { return buffer.getFloat64(0); case PostgreSQLDataType.timestampWithoutTimezone: case PostgreSQLDataType.timestampWithTimezone: - return new DateTime.utc(2000) - .add(new Duration(microseconds: buffer.getInt64(0))); + return DateTime.utc(2000) + .add(Duration(microseconds: buffer.getInt64(0))); case PostgreSQLDataType.date: - return new DateTime.utc(2000) - .add(new Duration(days: buffer.getInt32(0))); + return DateTime.utc(2000).add(Duration(days: buffer.getInt32(0))); case PostgreSQLDataType.json: { @@ -257,7 +246,7 @@ class PostgresBinaryDecoder extends Converter { case PostgreSQLDataType.uuid: { - final codeDash = "-".codeUnitAt(0); + final codeDash = '-'.codeUnitAt(0); final cipher = [ '0', @@ -281,7 +270,7 @@ class PostgresBinaryDecoder extends Converter { return cipher[value]; }; - final buf = new StringBuffer(); + final buf = StringBuffer(); for (var i = 0; i < buffer.lengthInBytes; i++) { final byteValue = buffer.getUint8(i); final upperByteValue = byteValue ~/ 16; diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 88f9db8..f46a49f 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -1,11 +1,12 @@ -import 'utf8_backed_string.dart'; import 'dart:typed_data'; -import 'query.dart'; -import 'constants.dart'; import 'package:buffer/buffer.dart'; import 'package:crypto/crypto.dart'; +import 'constants.dart'; +import 'query.dart'; +import 'utf8_backed_string.dart'; + abstract class ClientMessage { static const int FormatText = 0; static const int FormatBinary = 1; @@ -34,13 +35,13 @@ abstract class ClientMessage { void applyToBuffer(ByteDataWriter buffer); Uint8List asBytes() { - var buffer = new ByteDataWriter(); + final buffer = ByteDataWriter(); applyToBuffer(buffer); return buffer.toBytes(); } static Uint8List aggregateBytes(List messages) { - var buffer = new ByteDataWriter(); + final buffer = ByteDataWriter(); messages.forEach((cm) => cm.applyToBuffer(buffer)); return buffer.toBytes(); } @@ -48,22 +49,23 @@ abstract class ClientMessage { class StartupMessage extends ClientMessage { StartupMessage(String databaseName, String timeZone, {String username}) { - this.databaseName = new UTF8BackedString(databaseName); - this.timeZone = new UTF8BackedString(timeZone); + this.databaseName = UTF8BackedString(databaseName); + this.timeZone = UTF8BackedString(timeZone); if (username != null) { - this.username = new UTF8BackedString(username); + this.username = UTF8BackedString(username); } } - UTF8BackedString username = null; + UTF8BackedString username; UTF8BackedString databaseName; UTF8BackedString timeZone; ByteData buffer; + @override int get length { - var fixedLength = 53; - var variableLength = (username?.utf8Length ?? 0) + + final fixedLength = 53; + final variableLength = (username?.utf8Length ?? 0) + databaseName.utf8Length + timeZone.utf8Length + 3; @@ -71,6 +73,7 @@ class StartupMessage extends ClientMessage { return fixedLength + variableLength; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeInt32(length); buffer.writeInt32(ClientMessage.ProtocolVersion); @@ -95,19 +98,21 @@ class StartupMessage extends ClientMessage { class AuthMD5Message extends ClientMessage { AuthMD5Message(String username, String password, List saltBytes) { - var passwordHash = - md5.convert("${password}${username}".codeUnits).toString(); - var saltString = new String.fromCharCodes(saltBytes); - hashedAuthString = new UTF8BackedString( - "md5" + md5.convert("$passwordHash$saltString".codeUnits).toString()); + final passwordHash = md5.convert('$password$username'.codeUnits).toString(); + final saltString = String.fromCharCodes(saltBytes); + final md5Hash = + md5.convert('$passwordHash$saltString'.codeUnits).toString(); + hashedAuthString = UTF8BackedString('md5$md5Hash'); } UTF8BackedString hashedAuthString; + @override int get length { return 6 + hashedAuthString.utf8Length; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.PasswordIdentifier); buffer.writeUint32(length - 1); @@ -117,15 +122,17 @@ class AuthMD5Message extends ClientMessage { class QueryMessage extends ClientMessage { QueryMessage(String queryString) { - this.queryString = new UTF8BackedString(queryString); + this.queryString = UTF8BackedString(queryString); } UTF8BackedString queryString; + @override int get length { return 6 + queryString.utf8Length; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.QueryIdentifier); buffer.writeUint32(length - 1); @@ -134,18 +141,20 @@ class QueryMessage extends ClientMessage { } class ParseMessage extends ClientMessage { - ParseMessage(String statement, {String statementName: ""}) { - this.statement = new UTF8BackedString(statement); - this.statementName = new UTF8BackedString(statementName); + ParseMessage(String statement, {String statementName = ''}) { + this.statement = UTF8BackedString(statement); + this.statementName = UTF8BackedString(statementName); } UTF8BackedString statementName; UTF8BackedString statement; + @override int get length { return 9 + statement.utf8Length + statementName.utf8Length; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.ParseIdentifier); buffer.writeUint32(length - 1); @@ -157,16 +166,18 @@ class ParseMessage extends ClientMessage { } class DescribeMessage extends ClientMessage { - DescribeMessage({String statementName: ""}) { - this.statementName = new UTF8BackedString(statementName); + DescribeMessage({String statementName = ''}) { + this.statementName = UTF8BackedString(statementName); } UTF8BackedString statementName; + @override int get length { return 7 + statementName.utf8Length; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.DescribeIdentifier); buffer.writeUint32(length - 1); @@ -176,9 +187,9 @@ class DescribeMessage extends ClientMessage { } class BindMessage extends ClientMessage { - BindMessage(this.parameters, {String statementName: ""}) { + BindMessage(this.parameters, {String statementName = ''}) { typeSpecCount = parameters.where((p) => p.isBinary).length; - this.statementName = new UTF8BackedString(statementName); + this.statementName = UTF8BackedString(statementName); } List parameters; @@ -187,6 +198,7 @@ class BindMessage extends ClientMessage { int typeSpecCount; int _cachedLength; + @override int get length { if (_cachedLength == null) { var inputParameterElementCount = parameters.length; @@ -197,7 +209,8 @@ class BindMessage extends ClientMessage { _cachedLength = 15; _cachedLength += statementName.utf8Length; _cachedLength += inputParameterElementCount * 2; - _cachedLength += parameters.fold(0, (len, ParameterValue paramValue) { + _cachedLength += + parameters.fold(0, (len, ParameterValue paramValue) { if (paramValue.bytes == null) { return len + 4; } else { @@ -208,6 +221,7 @@ class BindMessage extends ClientMessage { return _cachedLength; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.BindIdentifier); buffer.writeUint32(length - 1); @@ -256,10 +270,12 @@ class BindMessage extends ClientMessage { class ExecuteMessage extends ClientMessage { ExecuteMessage(); + @override int get length { return 10; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.ExecuteIdentifier); buffer.writeUint32(length - 1); @@ -271,10 +287,12 @@ class ExecuteMessage extends ClientMessage { class SyncMessage extends ClientMessage { SyncMessage(); + @override int get length { return 5; } + @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.SyncIdentifier); buffer.writeUint32(4); diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 72dfe81..3b1eefd 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -4,15 +4,15 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import 'package:postgres/src/query_cache.dart'; -import 'package:postgres/src/execution_context.dart'; -import 'package:postgres/src/query_queue.dart'; +import 'package:buffer/buffer.dart'; +import 'client_messages.dart'; +import 'execution_context.dart'; import 'message_window.dart'; import 'query.dart'; - +import 'query_cache.dart'; +import 'query_queue.dart'; import 'server_messages.dart'; -import 'client_messages.dart'; part 'connection_fsm.dart'; @@ -38,18 +38,18 @@ class PostgreSQLConnection extends Object /// [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: null, - this.password: null, - this.timeoutInSeconds: 30, - this.queryTimeoutInSeconds: 30, - this.timeZone: "UTC", - this.useSSL: false}) { - _connectionState = new _PostgreSQLConnectionStateClosed(); + {this.username, + this.password, + this.timeoutInSeconds = 30, + this.queryTimeoutInSeconds = 30, + this.timeZone = 'UTC', + this.useSSL = false}) { + _connectionState = _PostgreSQLConnectionStateClosed(); _connectionState.connection = this; } final StreamController _notifications = - new StreamController.broadcast(); + StreamController.broadcast(); /// Hostname of database this connection refers to. final String host; @@ -92,7 +92,7 @@ class PostgreSQLConnection extends Object /// Whether or not this connection is open or not. /// /// This is `true` when this instance is first created and after it has been closed or encountered an unrecoverable error. - /// If a connection has already been opened and this value is now true, the connection cannot be reopened and a new instance + /// If a connection has already been opened and this value is now true, the connection cannot be reopened and a instance /// must be created. bool get isClosed => _connectionState is _PostgreSQLConnectionStateClosed; @@ -102,9 +102,9 @@ class PostgreSQLConnection extends Object /// Prior to connection, it is the empty map. final Map settings = {}; - QueryCache _cache = new QueryCache(); + final _cache = QueryCache(); Socket _socket; - MessageFramer _framer = new MessageFramer(); + MessageFramer _framer = MessageFramer(); int _processID; // ignore: unused_field int _secretKey; @@ -113,8 +113,10 @@ class PostgreSQLConnection extends Object bool _hasConnectedPreviously = false; _PostgreSQLConnectionState _connectionState; + @override PostgreSQLExecutionContext get _transaction => null; + @override PostgreSQLConnection get _connection => this; /// Establishes a connection with a PostgreSQL database. @@ -127,32 +129,31 @@ class PostgreSQLConnection extends Object /// opened and this method is called, an exception will be thrown. Future open() async { if (_hasConnectedPreviously) { - throw new PostgreSQLException( - "Attempting to reopen a closed connection. Create a new instance instead."); + throw PostgreSQLException( + 'Attempting to reopen a closed connection. Create a instance instead.'); } try { _hasConnectedPreviously = true; _socket = await Socket.connect(host, port) - .timeout(new Duration(seconds: timeoutInSeconds)); + .timeout(Duration(seconds: timeoutInSeconds)); - _framer = new MessageFramer(); + _framer = MessageFramer(); if (useSSL) { _socket = await _upgradeSocketToSSL(_socket, timeout: timeoutInSeconds); } - var connectionComplete = new Completer(); - _socket.listen(_readData, - onError: (err, st) => _close(err, st), onDone: () => _close()); + final connectionComplete = Completer(); + _socket.listen(_readData, onError: _close, onDone: _close); _transitionToState( - new _PostgreSQLConnectionStateSocketConnected(connectionComplete)); + _PostgreSQLConnectionStateSocketConnected(connectionComplete)); await connectionComplete.future - .timeout(new Duration(seconds: timeoutInSeconds)); + .timeout(Duration(seconds: timeoutInSeconds)); } on TimeoutException catch (e, st) { - final err = new PostgreSQLException( - "Failed to connect to database $host:$port/$databaseName failed to connect."); + final err = PostgreSQLException( + 'Failed to connect to database $host:$port/$databaseName failed to connect.'); await _close(err, st); rethrow; } catch (e, st) { @@ -195,22 +196,22 @@ class PostgreSQLConnection extends Object /// If specified, the final `"COMMIT"` query of the transaction will use /// [commitTimeoutInSeconds] as its timeout, otherwise the connection's /// default query timeout will be used. - Future transaction( - Future queryBlock(PostgreSQLExecutionContext connection), + Future transaction(Future queryBlock(PostgreSQLExecutionContext connection), {int commitTimeoutInSeconds}) async { if (isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw PostgreSQLException( + 'Attempting to execute query, but connection is not open.'); } - var proxy = new _TransactionProxy(this, queryBlock, commitTimeoutInSeconds); + final proxy = _TransactionProxy(this, queryBlock, commitTimeoutInSeconds); await _enqueue(proxy.beginQuery); return await proxy.completer.future; } - void cancelTransaction({String reason: null}) { + @override + void cancelTransaction({String reason}) { // Default is no-op } @@ -231,7 +232,7 @@ class PostgreSQLConnection extends Object } Future _close([dynamic error, StackTrace trace]) async { - _connectionState = new _PostgreSQLConnectionStateClosed(); + _connectionState = _PostgreSQLConnectionStateClosed(); await _socket?.close(); await _notifications?.close(); @@ -245,15 +246,15 @@ class PostgreSQLConnection extends Object // and the state node managing delivering data to the query no longer exists. Therefore, // as soon as a close occurs, we detach the data stream from anything that actually does // anything with that data. - _framer.addBytes(bytes); + _framer.addBytes(castBytes(bytes)); while (_framer.hasMessage) { - var msg = _framer.popMessage().message; + final msg = _framer.popMessage().message; try { if (msg is ErrorResponseMessage) { _transitionToState(_connectionState.onErrorResponse(msg)); } else if (msg is NotificationResponseMessage) { _notifications - .add(new Notification(msg.processID, msg.channel, msg.payload)); + .add(Notification(msg.processID, msg.channel, msg.payload)); } else { _transitionToState(_connectionState.onMessage(msg)); } @@ -263,41 +264,39 @@ class PostgreSQLConnection extends Object } } - Future _upgradeSocketToSSL(Socket originalSocket, {int timeout: 30}) { - var sslCompleter = new Completer(); - - originalSocket.listen( - (data) { - if (data.length != 1) { - sslCompleter.completeError(new PostgreSQLException( - "Could not initalize SSL connection, received unknown byte stream.")); - return; - } - - sslCompleter.complete(data.first); - }, - onDone: () => sslCompleter.completeError(new PostgreSQLException( - "Could not initialize SSL connection, connection closed during handshake.")), - onError: (err) { - sslCompleter.completeError(err); - }); - - var byteBuffer = new ByteData(8); + Future _upgradeSocketToSSL(Socket originalSocket, + {int timeout = 30}) { + final sslCompleter = Completer(); + + originalSocket.listen((data) { + if (data.length != 1) { + sslCompleter.completeError(PostgreSQLException( + 'Could not initalize SSL connection, received unknown byte stream.')); + return; + } + + sslCompleter.complete(data.first); + }, + onDone: () => sslCompleter.completeError(PostgreSQLException( + 'Could not initialize SSL connection, connection closed during handshake.')), + onError: sslCompleter.completeError); + + final byteBuffer = ByteData(8); byteBuffer.setUint32(0, 8); byteBuffer.setUint32(4, 80877103); originalSocket.add(byteBuffer.buffer.asUint8List()); return sslCompleter.future - .timeout(new Duration(seconds: timeout)) + .timeout(Duration(seconds: timeout)) .then((responseByte) { if (responseByte != 83) { - throw new PostgreSQLException( - "The database server is not accepting SSL connections."); + throw PostgreSQLException( + 'The database server is not accepting SSL connections.'); } return SecureSocket.secure(originalSocket, onBadCertificate: (certificate) => true) - .timeout(new Duration(seconds: timeout)); + .timeout(Duration(seconds: timeout)); }); } } @@ -327,26 +326,28 @@ class Notification { abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { - Map _tableOIDNameMap = {}; - QueryQueue _queue = new QueryQueue(); + final _tableOIDNameMap = {}; + final _queue = QueryQueue(); PostgreSQLConnection get _connection; PostgreSQLExecutionContext get _transaction; + @override int get queueSize => _queue.length; + @override Future>> query(String fmtString, - {Map substitutionValues: null, - bool allowReuse: true, + {Map substitutionValues, + bool allowReuse = true, int timeoutInSeconds}) async { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw PostgreSQLException( + 'Attempting to execute query, but connection is not open.'); } - var query = new Query>>( + final query = Query>>( fmtString, substitutionValues, _connection, _transaction); if (allowReuse) { query.statementIdentifier = _connection._cache.identifierForQuery(query); @@ -355,18 +356,19 @@ abstract class _PostgreSQLExecutionContextMixin return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } + @override Future>>> mappedResultsQuery( String fmtString, - {Map substitutionValues: null, - bool allowReuse: true, + {Map substitutionValues, + bool allowReuse = true, int timeoutInSeconds}) async { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw PostgreSQLException( + 'Attempting to execute query, but connection is not open.'); } - var query = new Query>>( + final query = Query>>( fmtString, substitutionValues, _connection, _transaction); if (allowReuse) { query.statementIdentifier = _connection._cache.identifierForQuery(query); @@ -377,29 +379,31 @@ abstract class _PostgreSQLExecutionContextMixin return _mapifyRows(rows, query.fieldDescriptions); } + @override Future execute(String fmtString, - {Map substitutionValues: null, int timeoutInSeconds}) { + {Map substitutionValues, int timeoutInSeconds}) { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { - throw new PostgreSQLException( - "Attempting to execute query, but connection is not open."); + throw PostgreSQLException( + 'Attempting to execute query, but connection is not open.'); } - var query = - new Query(fmtString, substitutionValues, _connection, _transaction) + final query = + Query(fmtString, substitutionValues, _connection, _transaction) ..onlyReturnAffectedRowCount = true; return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } - void cancelTransaction({String reason: null}); + @override + void cancelTransaction({String reason}); Future>>> _mapifyRows( List> rows, List columns) async { //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName - final tableOIDs = new Set.from(columns.map((f) => f.tableID)); + final tableOIDs = Set.from(columns.map((f) => f.tableID)); final List unresolvedTableOIDs = tableOIDs .where((oid) => oid != null && oid > 0 && !_tableOIDNameMap.containsKey(oid)) @@ -416,10 +420,8 @@ abstract class _PostgreSQLExecutionContextMixin final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); return rows.map((row) { - var rowMap = new Map>.fromIterable( - tableNames, - key: (name) => name, - value: (_) => {}); + final rowMap = Map>.fromIterable(tableNames, + key: (name) => name as String, value: (_) => {}); final iterator = columns.iterator; row.forEach((column) { @@ -433,7 +435,7 @@ abstract class _PostgreSQLExecutionContextMixin } Future _resolveTableOIDs(List oids) async { - final unresolvedIDString = oids.join(","); + final unresolvedIDString = oids.join(','); final orderedTableNames = await query( "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); @@ -441,18 +443,18 @@ abstract class _PostgreSQLExecutionContextMixin orderedTableNames.forEach((tableName) { iterator.moveNext(); if (tableName.first != null) { - _tableOIDNameMap[iterator.current] = tableName.first; + _tableOIDNameMap[iterator.current] = tableName.first as String; } }); } - Future _enqueue(Query query, {int timeoutInSeconds: 30}) async { + Future _enqueue(Query query, {int timeoutInSeconds = 30}) async { if (_queue.add(query)) { _connection._transitionToState(_connection._connectionState.awake()); try { final result = - await query.future.timeout(new Duration(seconds: timeoutInSeconds)); + await query.future.timeout(Duration(seconds: timeoutInSeconds)); _connection._cache.add(query); _queue.remove(query); return result; @@ -466,7 +468,7 @@ abstract class _PostgreSQLExecutionContextMixin // the caller behaves correctly in this condition. otherwise, // the caller would complete synchronously. This future // will always complete as a cancellation error. - return new Future(() async => query.future); + return Future(() async => query.future); } } diff --git a/lib/src/connection_fsm.dart b/lib/src/connection_fsm.dart index 27409ad..3dbb2b4 100644 --- a/lib/src/connection_fsm.dart +++ b/lib/src/connection_fsm.dart @@ -16,11 +16,11 @@ abstract class _PostgreSQLConnectionState { } _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { - var exception = new PostgreSQLException._(message.fields); + final exception = PostgreSQLException._(message.fields); if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { - return new _PostgreSQLConnectionStateClosed(); + return _PostgreSQLConnectionStateClosed(); } return this; @@ -45,8 +45,9 @@ class _PostgreSQLConnectionStateSocketConnected Completer completer; + @override _PostgreSQLConnectionState onEnter() { - var startupMessage = new StartupMessage( + final startupMessage = StartupMessage( connection.databaseName, connection.timeZone, username: connection.username); @@ -55,30 +56,32 @@ class _PostgreSQLConnectionStateSocketConnected return this; } + @override _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { - var exception = new PostgreSQLException._(message.fields); + final exception = PostgreSQLException._(message.fields); completer.completeError(exception); - return new _PostgreSQLConnectionStateClosed(); + return _PostgreSQLConnectionStateClosed(); } + @override _PostgreSQLConnectionState onMessage(ServerMessage message) { - AuthenticationMessage authMessage = message; + final authMessage = message as AuthenticationMessage; // Pass on the pending op to subsequent stages if (authMessage.type == AuthenticationMessage.KindOK) { - return new _PostgreSQLConnectionStateAuthenticated(completer); + return _PostgreSQLConnectionStateAuthenticated(completer); } else if (authMessage.type == AuthenticationMessage.KindMD5Password) { connection._salt = authMessage.salt; - return new _PostgreSQLConnectionStateAuthenticating(completer); + return _PostgreSQLConnectionStateAuthenticating(completer); } - completer.completeError(new PostgreSQLException( - "Unsupported authentication type ${authMessage.type}, closing connection.")); + completer.completeError(PostgreSQLException( + 'Unsupported authentication type ${authMessage.type}, closing connection.')); - return new _PostgreSQLConnectionStateClosed(); + return _PostgreSQLConnectionStateClosed(); } } @@ -92,8 +95,9 @@ class _PostgreSQLConnectionStateAuthenticating Completer completer; + @override _PostgreSQLConnectionState onEnter() { - var authMessage = new AuthMD5Message( + final authMessage = AuthMD5Message( connection.username, connection.password, connection._salt); connection._socket.add(authMessage.asBytes()); @@ -101,14 +105,16 @@ class _PostgreSQLConnectionStateAuthenticating return this; } + @override _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { - var exception = new PostgreSQLException._(message.fields); + final exception = PostgreSQLException._(message.fields); completer.completeError(exception); - return new _PostgreSQLConnectionStateClosed(); + return _PostgreSQLConnectionStateClosed(); } + @override _PostgreSQLConnectionState onMessage(ServerMessage message) { if (message is ParameterStatusMessage) { connection.settings[message.name] = message.value; @@ -117,7 +123,7 @@ class _PostgreSQLConnectionStateAuthenticating connection._secretKey = message.secretKey; } else if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateIdle) { - return new _PostgreSQLConnectionStateIdle(openCompleter: completer); + return _PostgreSQLConnectionStateIdle(openCompleter: completer); } } @@ -135,14 +141,16 @@ class _PostgreSQLConnectionStateAuthenticated Completer completer; + @override _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { - var exception = new PostgreSQLException._(message.fields); + final exception = PostgreSQLException._(message.fields); completer.completeError(exception); - return new _PostgreSQLConnectionStateClosed(); + return _PostgreSQLConnectionStateClosed(); } + @override _PostgreSQLConnectionState onMessage(ServerMessage message) { if (message is ParameterStatusMessage) { connection.settings[message.name] = message.value; @@ -151,7 +159,7 @@ class _PostgreSQLConnectionStateAuthenticated connection._secretKey = message.secretKey; } else if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateIdle) { - return new _PostgreSQLConnectionStateIdle(openCompleter: completer); + return _PostgreSQLConnectionStateIdle(openCompleter: completer); } } @@ -168,8 +176,9 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { Completer openCompleter; + @override _PostgreSQLConnectionState awake() { - var pendingQuery = connection._queue.pending; + final pendingQuery = connection._queue.pending; if (pendingQuery != null) { return processQuery(pendingQuery); } @@ -181,29 +190,31 @@ class _PostgreSQLConnectionStateIdle extends _PostgreSQLConnectionState { try { if (q.onlyReturnAffectedRowCount) { q.sendSimple(connection._socket); - return new _PostgreSQLConnectionStateBusy(q); + return _PostgreSQLConnectionStateBusy(q); } final cached = connection._cache[q.statement]; q.sendExtended(connection._socket, cacheQuery: cached); - return new _PostgreSQLConnectionStateBusy(q); + return _PostgreSQLConnectionStateBusy(q); } catch (e, st) { scheduleMicrotask(() { q.completeError(e, st); - connection._transitionToState(new _PostgreSQLConnectionStateIdle()); + connection._transitionToState(_PostgreSQLConnectionStateIdle()); }); - return new _PostgreSQLConnectionStateDeferredFailure(); + return _PostgreSQLConnectionStateDeferredFailure(); } } + @override _PostgreSQLConnectionState onEnter() { openCompleter?.complete(); return awake(); } + @override _PostgreSQLConnectionState onMessage(ServerMessage message) { return this; } @@ -217,24 +228,26 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { _PostgreSQLConnectionStateBusy(this.query); Query query; - PostgreSQLException returningException = null; + PostgreSQLException returningException; int rowsAffected = 0; + @override _PostgreSQLConnectionState onErrorResponse(ErrorResponseMessage message) { // If we get an error here, then we should eat the rest of the messages // and we are always confirmed to get a _ReadyForQueryMessage to finish up. // We should only report the error once that is done. - var exception = new PostgreSQLException._(message.fields); + final exception = PostgreSQLException._(message.fields); returningException ??= exception; if (exception.severity == PostgreSQLSeverity.fatal || exception.severity == PostgreSQLSeverity.panic) { - return new _PostgreSQLConnectionStateClosed(); + return _PostgreSQLConnectionStateClosed(); } return this; } + @override _PostgreSQLConnectionState onMessage(ServerMessage message) { // We ignore NoData, as it doesn't tell us anything we don't already know // or care about. @@ -244,8 +257,8 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { if (message is ReadyForQueryMessage) { if (message.state == ReadyForQueryMessage.StateTransactionError) { query.completeError(returningException); - return new _PostgreSQLConnectionStateReadyInTransaction( - query.transaction); + return _PostgreSQLConnectionStateReadyInTransaction( + query.transaction as _TransactionProxy); } if (returningException != null) { @@ -255,11 +268,11 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } if (message.state == ReadyForQueryMessage.StateTransaction) { - return new _PostgreSQLConnectionStateReadyInTransaction( - query.transaction); + return _PostgreSQLConnectionStateReadyInTransaction( + query.transaction as _TransactionProxy); } - return new _PostgreSQLConnectionStateIdle(); + return _PostgreSQLConnectionStateIdle(); } else if (message is CommandCompleteMessage) { rowsAffected = message.rowsAffected; } else if (message is RowDescriptionMessage) { @@ -267,7 +280,7 @@ class _PostgreSQLConnectionStateBusy extends _PostgreSQLConnectionState { } else if (message is DataRowMessage) { query.addRow(message.values); } else if (message is ParameterDescriptionMessage) { - var validationException = + final validationException = query.validateParameters(message.parameterTypeIDs); if (validationException != null) { query.cache = null; @@ -287,12 +300,14 @@ class _PostgreSQLConnectionStateReadyInTransaction _TransactionProxy transaction; + @override _PostgreSQLConnectionState onEnter() { return awake(); } + @override _PostgreSQLConnectionState awake() { - var pendingQuery = transaction._queue.pending; + final pendingQuery = transaction._queue.pending; if (pendingQuery != null) { return processQuery(pendingQuery); } @@ -304,13 +319,13 @@ class _PostgreSQLConnectionStateReadyInTransaction try { if (q.onlyReturnAffectedRowCount) { q.sendSimple(connection._socket); - return new _PostgreSQLConnectionStateBusy(q); + return _PostgreSQLConnectionStateBusy(q); } final cached = connection._cache[q.statement]; q.sendExtended(connection._socket, cacheQuery: cached); - return new _PostgreSQLConnectionStateBusy(q); + return _PostgreSQLConnectionStateBusy(q); } catch (e, st) { scheduleMicrotask(() { q.completeError(e, st); diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index e0a5bc7..44e6a00 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -34,16 +34,13 @@ enum PostgreSQLSeverity { /// Exception thrown by [PostgreSQLConnection] instances. class PostgreSQLException implements Exception { - PostgreSQLException(String message, - {PostgreSQLSeverity severity: PostgreSQLSeverity.error, - this.stackTrace}) { - this.severity = severity; - this.message = message; - code = ""; + PostgreSQLException(this.message, + {this.severity = PostgreSQLSeverity.error, this.stackTrace}) { + code = ''; } PostgreSQLException._(List errorFields, {this.stackTrace}) { - var finder = (int identifer) => (errorFields.firstWhere( + final finder = (int identifer) => (errorFields.firstWhere( (ErrorField e) => e.identificationToken == identifer, orElse: () => null)); @@ -111,27 +108,28 @@ class PostgreSQLException implements Exception { /// A [StackTrace] if available. StackTrace stackTrace; + @override String toString() { - var buff = new StringBuffer("$severity $code: $message "); + final buff = StringBuffer('$severity $code: $message '); if (detail != null) { - buff.write("Detail: $detail "); + buff.write('Detail: $detail '); } if (hint != null) { - buff.write("Hint: $hint "); + buff.write('Hint: $hint '); } if (tableName != null) { - buff.write("Table: $tableName "); + buff.write('Table: $tableName '); } if (columnName != null) { - buff.write("Column: $columnName "); + buff.write('Column: $columnName '); } if (constraintName != null) { - buff.write("Constraint $constraintName "); + buff.write('Constraint $constraintName '); } return buff.toString(); diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 3ec8bed..e526625 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -1,8 +1,9 @@ import 'dart:async'; + +import 'connection.dart'; import 'query.dart'; -import 'types.dart'; import 'substituter.dart'; -import 'connection.dart'; +import 'types.dart'; abstract class PostgreSQLExecutionContext { /// Returns this context queue size @@ -29,8 +30,8 @@ abstract class PostgreSQLExecutionContext { /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. Future>> query(String fmtString, - {Map substitutionValues: null, - bool allowReuse: true, + {Map substitutionValues, + bool allowReuse = true, int timeoutInSeconds}); /// Executes a query on this context. @@ -41,13 +42,13 @@ abstract class PostgreSQLExecutionContext { /// for executing queries in the PostgreSQL protocol; [query] is preferred for queries that will be executed more than once, will contain user input, /// or return rows. Future execute(String fmtString, - {Map substitutionValues: null, int timeoutInSeconds}); + {Map substitutionValues, int timeoutInSeconds}); /// Cancels a transaction on this context. /// /// If this context is an instance of [PostgreSQLConnection], this method has no effect. If the context is a transaction context (passed as the argument /// to [PostgreSQLConnection.transaction]), this will rollback the transaction. - void cancelTransaction({String reason: null}); + void cancelTransaction({String reason}); /// Executes a query on this connection and returns each row as a [Map]. /// @@ -81,7 +82,7 @@ abstract class PostgreSQLExecutionContext { /// ] Future>>> mappedResultsQuery( String fmtString, - {Map substitutionValues: null, - bool allowReuse: true, + {Map substitutionValues, + bool allowReuse = true, int timeoutInSeconds}); } diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 130f044..3c0d193 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -8,19 +8,19 @@ import 'server_messages.dart'; class MessageFrame { static const int HeaderByteSize = 5; static Map messageTypeMap = { - 49: () => new ParseCompleteMessage(), - 50: () => new BindCompleteMessage(), - 65: () => new NotificationResponseMessage(), - 67: () => new CommandCompleteMessage(), - 68: () => new DataRowMessage(), - 69: () => new ErrorResponseMessage(), - 75: () => new BackendKeyMessage(), - 82: () => new AuthenticationMessage(), - 83: () => new ParameterStatusMessage(), - 84: () => new RowDescriptionMessage(), - 90: () => new ReadyForQueryMessage(), - 110: () => new NoDataMessage(), - 116: () => new ParameterDescriptionMessage() + 49: () => ParseCompleteMessage(), + 50: () => BindCompleteMessage(), + 65: () => NotificationResponseMessage(), + 67: () => CommandCompleteMessage(), + 68: () => DataRowMessage(), + 69: () => ErrorResponseMessage(), + 75: () => BackendKeyMessage(), + 82: () => AuthenticationMessage(), + 83: () => ParameterStatusMessage(), + 84: () => RowDescriptionMessage(), + 90: () => ReadyForQueryMessage(), + 110: () => NoDataMessage(), + 116: () => ParameterDescriptionMessage() }; bool get hasReadHeader => type != null; @@ -31,19 +31,19 @@ class MessageFrame { Uint8List data; ServerMessage get message { - var msgMaker = - messageTypeMap[type] ?? () => new UnknownMessage()..code = type; + final msgMaker = + messageTypeMap[type] ?? () => UnknownMessage()..code = type; - ServerMessage msg = msgMaker(); + final msg = msgMaker() as ServerMessage; msg.readBytes(data); return msg; } } class MessageFramer { - final _reader = new ByteDataReader(); - MessageFrame messageInProgress = new MessageFrame(); - final messageQueue = new Queue(); + final _reader = ByteDataReader(); + MessageFrame messageInProgress = MessageFrame(); + final messageQueue = Queue(); void addBytes(Uint8List bytes) { _reader.add(bytes); @@ -65,7 +65,7 @@ class MessageFramer { if (messageInProgress.isComplete) { messageQueue.add(messageInProgress); - messageInProgress = new MessageFrame(); + messageInProgress = MessageFrame(); evaluateNextMessage = true; } } diff --git a/lib/src/query.dart b/lib/src/query.dart index d99d455..b76247b 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -3,14 +3,15 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:postgres/src/binary_codec.dart'; -import 'package:postgres/src/execution_context.dart'; +import 'package:buffer/buffer.dart'; -import 'package:postgres/src/text_codec.dart'; -import 'types.dart'; +import 'binary_codec.dart'; +import 'client_messages.dart'; import 'connection.dart'; +import 'execution_context.dart'; import 'substituter.dart'; -import 'client_messages.dart'; +import 'text_codec.dart'; +import 'types.dart'; class Query { Query(this.statement, this.substitutionValues, this.connection, @@ -32,24 +33,25 @@ class Query { CachedQuery cache; - Completer _onComplete = new Completer.sync(); + final _onComplete = Completer.sync(); List _fieldDescriptions; List get fieldDescriptions => _fieldDescriptions; - void set fieldDescriptions(List fds) { + set fieldDescriptions(List fds) { _fieldDescriptions = fds; cache?.fieldDescriptions = fds; } void sendSimple(Socket socket) { - var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues); - var queryMessage = new QueryMessage(sqlString); + final sqlString = + PostgreSQLFormat.substitute(statement, substitutionValues); + final queryMessage = QueryMessage(sqlString); socket.add(queryMessage.asBytes()); } - void sendExtended(Socket socket, {CachedQuery cacheQuery: null}) { + void sendExtended(Socket socket, {CachedQuery cacheQuery}) { if (cacheQuery != null) { fieldDescriptions = cacheQuery.fieldDescriptions; sendCachedQuery(socket, cacheQuery, substitutionValues); @@ -57,31 +59,31 @@ class Query { return; } - String statementName = (statementIdentifier ?? ""); - var formatIdentifiers = []; - var sqlString = PostgreSQLFormat.substitute(statement, substitutionValues, + final statementName = statementIdentifier ?? ''; + final formatIdentifiers = []; + final sqlString = PostgreSQLFormat.substitute(statement, substitutionValues, replace: (PostgreSQLFormatIdentifier identifier, int index) { formatIdentifiers.add(identifier); - return "\$$index"; + return '\$$index'; }); specifiedParameterTypeCodes = formatIdentifiers.map((i) => i.type).toList(); - var parameterList = formatIdentifiers - .map((id) => new ParameterValue(id, substitutionValues)) + final parameterList = formatIdentifiers + .map((id) => ParameterValue(id, substitutionValues)) .toList(); - var messages = [ - new ParseMessage(sqlString, statementName: statementName), - new DescribeMessage(statementName: statementName), - new BindMessage(parameterList, statementName: statementName), - new ExecuteMessage(), - new SyncMessage() + final messages = [ + ParseMessage(sqlString, statementName: statementName), + DescribeMessage(statementName: statementName), + BindMessage(parameterList, statementName: statementName), + ExecuteMessage(), + SyncMessage(), ]; if (statementIdentifier != null) { - cache = new CachedQuery(statementIdentifier, formatIdentifiers); + cache = CachedQuery(statementIdentifier, formatIdentifiers); } socket.add(ClientMessage.aggregateBytes(messages)); @@ -89,23 +91,23 @@ class Query { void sendCachedQuery(Socket socket, CachedQuery cacheQuery, Map substitutionValues) { - var statementName = cacheQuery.preparedStatementName; - var parameterList = cacheQuery.orderedParameters - .map((identifier) => new ParameterValue(identifier, substitutionValues)) + final statementName = cacheQuery.preparedStatementName; + final parameterList = cacheQuery.orderedParameters + .map((identifier) => ParameterValue(identifier, substitutionValues)) .toList(); - var bytes = ClientMessage.aggregateBytes([ - new BindMessage(parameterList, statementName: statementName), - new ExecuteMessage(), - new SyncMessage() + final bytes = ClientMessage.aggregateBytes([ + BindMessage(parameterList, statementName: statementName), + ExecuteMessage(), + SyncMessage() ]); socket.add(bytes); } PostgreSQLException validateParameters(List parameterTypeIDs) { - var actualParameterTypeCodeIterator = parameterTypeIDs.iterator; - var parametersAreMismatched = + final actualParameterTypeCodeIterator = parameterTypeIDs.iterator; + final parametersAreMismatched = specifiedParameterTypeCodes.map((specifiedType) { actualParameterTypeCodeIterator.moveNext(); @@ -119,8 +121,8 @@ class Query { }).any((v) => v == false); if (parametersAreMismatched) { - return new PostgreSQLException( - "Specified parameter types do not match column parameter types in query ${statement}"); + return PostgreSQLException( + 'Specified parameter types do not match column parameter types in query $statement'); } return null; @@ -131,8 +133,8 @@ class Query { return; } - var iterator = fieldDescriptions.iterator; - var lazyDecodedData = rawRowData.map((bd) { + final iterator = fieldDescriptions.iterator; + final lazyDecodedData = rawRowData.map((bd) { iterator.moveNext(); return iterator.current.converter @@ -163,6 +165,7 @@ class Query { _onComplete.completeError(error, stackTrace); } + @override String toString() => statement; } @@ -184,24 +187,24 @@ class ParameterValue { factory ParameterValue(PostgreSQLFormatIdentifier identifier, Map substitutionValues) { if (identifier.type == null) { - return new ParameterValue.text(substitutionValues[identifier.name]); + return ParameterValue.text(substitutionValues[identifier.name]); } - return new ParameterValue.binary( + return ParameterValue.binary( substitutionValues[identifier.name], identifier.type); } ParameterValue.binary(dynamic value, PostgreSQLDataType postgresType) : isBinary = true { - final converter = new PostgresBinaryEncoder(postgresType); + final converter = PostgresBinaryEncoder(postgresType); bytes = converter.convert(value); length = bytes?.length ?? 0; } ParameterValue.text(dynamic value) : isBinary = false { if (value != null) { - final converter = new PostgresTextEncoder(false); - bytes = utf8.encode(converter.convert(value)); + final converter = PostgresTextEncoder(false); + bytes = castBytes(utf8.encode(converter.convert(value))); } length = bytes?.length; } @@ -225,9 +228,9 @@ class FieldDescription { String resolvedTableName; int parse(ByteData byteData, int initialOffset) { - var offset = initialOffset; - var buf = new StringBuffer(); - var byte = 0; + int offset = initialOffset; + final buf = StringBuffer(); + int byte = 0; do { byte = byteData.getUint8(offset); offset += 1; @@ -251,13 +254,14 @@ class FieldDescription { formatCode = byteData.getUint16(offset); offset += 2; - converter = new PostgresBinaryDecoder(typeID); + converter = PostgresBinaryDecoder(typeID); return offset; } + @override String toString() { - return "$fieldName $tableID $columnID $typeID $dataTypeSize $typeModifier $formatCode"; + return '$fieldName $tableID $columnID $typeID $dataTypeSize $typeModifier $formatCode'; } } @@ -270,50 +274,50 @@ class PostgreSQLFormatToken { PostgreSQLFormatToken(this.type); PostgreSQLFormatTokenType type; - StringBuffer buffer = new StringBuffer(); + StringBuffer buffer = StringBuffer(); } class PostgreSQLFormatIdentifier { static Map typeStringToCodeMap = { - "text": PostgreSQLDataType.text, - "int2": PostgreSQLDataType.smallInteger, - "int4": PostgreSQLDataType.integer, - "int8": PostgreSQLDataType.bigInteger, - "float4": PostgreSQLDataType.real, - "float8": PostgreSQLDataType.double, - "boolean": PostgreSQLDataType.boolean, - "date": PostgreSQLDataType.date, - "timestamp": PostgreSQLDataType.timestampWithoutTimezone, - "timestamptz": PostgreSQLDataType.timestampWithTimezone, - "jsonb": PostgreSQLDataType.json, - "bytea": PostgreSQLDataType.byteArray, - "name": PostgreSQLDataType.name, - "uuid": PostgreSQLDataType.uuid + 'text': PostgreSQLDataType.text, + 'int2': PostgreSQLDataType.smallInteger, + 'int4': PostgreSQLDataType.integer, + 'int8': PostgreSQLDataType.bigInteger, + 'float4': PostgreSQLDataType.real, + 'float8': PostgreSQLDataType.double, + 'boolean': PostgreSQLDataType.boolean, + 'date': PostgreSQLDataType.date, + 'timestamp': PostgreSQLDataType.timestampWithoutTimezone, + 'timestamptz': PostgreSQLDataType.timestampWithTimezone, + 'jsonb': PostgreSQLDataType.json, + 'bytea': PostgreSQLDataType.byteArray, + 'name': PostgreSQLDataType.name, + 'uuid': PostgreSQLDataType.uuid }; PostgreSQLFormatIdentifier(String t) { - var components = t.split("::"); + final components = t.split('::'); if (components.length > 1) { - typeCast = components.sublist(1).join(""); + typeCast = components.sublist(1).join(''); } - var variableComponents = components.first.split(":"); + final variableComponents = components.first.split(':'); if (variableComponents.length == 1) { name = variableComponents.first; } else if (variableComponents.length == 2) { name = variableComponents.first; - var dataTypeString = variableComponents.last; + final dataTypeString = variableComponents.last; if (dataTypeString != null) { type = typeStringToCodeMap[dataTypeString]; if (type == null) { - throw new FormatException( + throw FormatException( "Invalid type code in substitution variable '$t'"); } } } else { - throw new FormatException( - "Invalid format string identifier, must contain identifier name and optionally one data type in format '@identifier:dataType' (offending identifier: ${t})"); + throw FormatException( + "Invalid format string identifier, must contain identifier name and optionally one data type in format '@identifier:dataType' (offending identifier: $t)"); } // Strip @ diff --git a/lib/src/query_cache.dart b/lib/src/query_cache.dart index c7fef2c..221c8f9 100644 --- a/lib/src/query_cache.dart +++ b/lib/src/query_cache.dart @@ -1,4 +1,4 @@ -import 'package:postgres/src/query.dart'; +import 'query.dart'; class QueryCache { final Map queries = {}; @@ -14,7 +14,7 @@ class QueryCache { } } - operator [](String statementId) { + CachedQuery operator [](String statementId) { if (statementId == null) { return null; } @@ -23,12 +23,12 @@ class QueryCache { } String identifierForQuery(Query query) { - var existing = queries[query.statement]; + final existing = queries[query.statement]; if (existing != null) { return existing.preparedStatementName; } - var string = "$idCounter".padLeft(12, "0"); + final string = '$idCounter'.padLeft(12, '0'); idCounter++; diff --git a/lib/src/query_queue.dart b/lib/src/query_queue.dart index 53d12d0..1007d84 100644 --- a/lib/src/query_queue.dart +++ b/lib/src/query_queue.dart @@ -1,16 +1,17 @@ import 'dart:async'; import 'dart:collection'; -import 'package:postgres/postgres.dart'; -import 'package:postgres/src/query.dart'; +import '../postgres.dart'; + +import 'query.dart'; class QueryQueue extends ListBase> implements List> { List> _inner = []; bool _isCancelled = false; - PostgreSQLException get _cancellationException => new PostgreSQLException( - "Query cancelled due to the database connection closing."); + PostgreSQLException get _cancellationException => PostgreSQLException( + 'Query cancelled due to the database connection closing.'); Query get pending { if (_inner.isEmpty) { diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index a50bcab..2cfbc47 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -9,11 +9,12 @@ abstract class ServerMessage { } class ErrorResponseMessage implements ServerMessage { - List fields = [new ErrorField()]; + List fields = [ErrorField()]; + @override void readBytes(Uint8List bytes) { - var lastByteRemovedList = - new Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); + final lastByteRemovedList = + Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); lastByteRemovedList.forEach((byte) { if (byte != 0) { @@ -21,7 +22,7 @@ class ErrorResponseMessage implements ServerMessage { return; } - fields.add(new ErrorField()); + fields.add(ErrorField()); }); } } @@ -40,12 +41,13 @@ class AuthenticationMessage implements ServerMessage { List salt; + @override void readBytes(Uint8List bytes) { - var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); + final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); type = view.getUint32(0); if (type == KindMD5Password) { - salt = new List(4); + salt = List(4); for (var i = 0; i < 4; i++) { salt[i] = view.getUint8(4 + i); } @@ -57,6 +59,7 @@ class ParameterStatusMessage extends ServerMessage { String name; String value; + @override void readBytes(Uint8List bytes) { name = utf8.decode(bytes.sublist(0, bytes.indexOf(0))); value = @@ -65,12 +68,13 @@ class ParameterStatusMessage extends ServerMessage { } class ReadyForQueryMessage extends ServerMessage { - static const String StateIdle = "I"; - static const String StateTransaction = "T"; - static const String StateTransactionError = "E"; + static const String StateIdle = 'I'; + static const String StateTransaction = 'T'; + static const String StateTransactionError = 'E'; String state; + @override void readBytes(Uint8List bytes) { state = utf8.decode(bytes); } @@ -80,8 +84,9 @@ class BackendKeyMessage extends ServerMessage { int processID; int secretKey; + @override void readBytes(Uint8List bytes) { - var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); + final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); processID = view.getUint32(0); secretKey = view.getUint32(4); } @@ -90,15 +95,16 @@ class BackendKeyMessage extends ServerMessage { class RowDescriptionMessage extends ServerMessage { List fieldDescriptions; + @override void readBytes(Uint8List bytes) { - var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); - var offset = 0; - var fieldCount = view.getInt16(offset); + final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); + int offset = 0; + final fieldCount = view.getInt16(offset); offset += 2; fieldDescriptions = []; for (var i = 0; i < fieldCount; i++) { - var rowDesc = new FieldDescription(); + final rowDesc = FieldDescription(); offset = rowDesc.parse(view, offset); fieldDescriptions.add(rowDesc); } @@ -108,30 +114,32 @@ class RowDescriptionMessage extends ServerMessage { class DataRowMessage extends ServerMessage { List values = []; + @override void readBytes(Uint8List bytes) { - var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); - var offset = 0; - var fieldCount = view.getInt16(offset); + final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); + int offset = 0; + final fieldCount = view.getInt16(offset); offset += 2; for (var i = 0; i < fieldCount; i++) { - var dataSize = view.getInt32(offset); + final dataSize = view.getInt32(offset); offset += 4; if (dataSize == 0) { - values.add(new ByteData(0)); + values.add(ByteData(0)); } else if (dataSize == -1) { values.add(null); } else { - var rawBytes = new ByteData.view( - bytes.buffer, bytes.offsetInBytes + offset, dataSize); + final rawBytes = + ByteData.view(bytes.buffer, bytes.offsetInBytes + offset, dataSize); values.add(rawBytes); offset += dataSize; } } } - String toString() => "Data Row Message: ${values}"; + @override + String toString() => 'Data Row Message: $values'; } class NotificationResponseMessage extends ServerMessage { @@ -139,8 +147,9 @@ class NotificationResponseMessage extends ServerMessage { String channel; String payload; + @override void readBytes(Uint8List bytes) { - var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); + final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); processID = view.getUint32(0); channel = utf8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); payload = utf8 @@ -151,14 +160,15 @@ class NotificationResponseMessage extends ServerMessage { class CommandCompleteMessage extends ServerMessage { int rowsAffected; - static RegExp identifierExpression = new RegExp(r"[A-Z ]*"); + static RegExp identifierExpression = RegExp(r'[A-Z ]*'); + @override void readBytes(Uint8List bytes) { - var str = utf8.decode(bytes.sublist(0, bytes.length - 1)); + final str = utf8.decode(bytes.sublist(0, bytes.length - 1)); - var match = identifierExpression.firstMatch(str); + final match = identifierExpression.firstMatch(str); if (match.end < str.length) { - rowsAffected = int.parse(str.split(" ").last); + rowsAffected = int.parse(str.split(' ').last); } else { rowsAffected = 0; } @@ -166,30 +176,35 @@ class CommandCompleteMessage extends ServerMessage { } class ParseCompleteMessage extends ServerMessage { + @override void readBytes(Uint8List bytes) {} - String toString() => "Parse Complete Message"; + @override + String toString() => 'Parse Complete Message'; } class BindCompleteMessage extends ServerMessage { + @override void readBytes(Uint8List bytes) {} - String toString() => "Bind Complete Message"; + @override + String toString() => 'Bind Complete Message'; } class ParameterDescriptionMessage extends ServerMessage { List parameterTypeIDs; + @override void readBytes(Uint8List bytes) { - var view = new ByteData.view(bytes.buffer, bytes.offsetInBytes); + final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); - var offset = 0; - var count = view.getUint16(0); + int offset = 0; + final count = view.getUint16(0); offset += 2; parameterTypeIDs = []; for (var i = 0; i < count; i++) { - var v = view.getUint32(offset); + final v = view.getUint32(offset); offset += 4; parameterTypeIDs.add(v); } @@ -197,15 +212,18 @@ class ParameterDescriptionMessage extends ServerMessage { } class NoDataMessage extends ServerMessage { + @override void readBytes(Uint8List bytes) {} - String toString() => "No Data Message"; + @override + String toString() => 'No Data Message'; } class UnknownMessage extends ServerMessage { Uint8List bytes; int code; + @override void readBytes(Uint8List bytes) { this.bytes = bytes; } @@ -256,21 +274,21 @@ class ErrorField { static PostgreSQLSeverity severityFromString(String str) { switch (str) { - case "ERROR": + case 'ERROR': return PostgreSQLSeverity.error; - case "FATAL": + case 'FATAL': return PostgreSQLSeverity.fatal; - case "PANIC": + case 'PANIC': return PostgreSQLSeverity.panic; - case "WARNING": + case 'WARNING': return PostgreSQLSeverity.warning; - case "NOTICE": + case 'NOTICE': return PostgreSQLSeverity.notice; - case "DEBUG": + case 'DEBUG': return PostgreSQLSeverity.debug; - case "INFO": + case 'INFO': return PostgreSQLSeverity.info; - case "LOG": + case 'LOG': return PostgreSQLSeverity.log; } @@ -280,7 +298,7 @@ class ErrorField { int identificationToken; String get text => _buffer.toString(); - StringBuffer _buffer = new StringBuffer(); + final _buffer = StringBuffer(); void add(int byte) { if (identificationToken == null) { diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 4682fca..41505f7 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -1,99 +1,98 @@ -import 'package:postgres/src/text_codec.dart'; -import 'types.dart'; import 'query.dart'; +import 'text_codec.dart'; +import 'types.dart'; class PostgreSQLFormat { - static int _AtSignCodeUnit = "@".codeUnitAt(0); + static final int _atSignCodeUnit = '@'.codeUnitAt(0); - static String id(String name, {PostgreSQLDataType type: null}) { + static String id(String name, {PostgreSQLDataType type}) { if (type != null) { - return "@$name:${dataTypeStringForDataType(type)}"; + return '@$name:${dataTypeStringForDataType(type)}'; } - return "@$name"; + return '@$name'; } static String dataTypeStringForDataType(PostgreSQLDataType dt) { switch (dt) { case PostgreSQLDataType.text: - return "text"; + return 'text'; case PostgreSQLDataType.integer: - return "int4"; + return 'int4'; case PostgreSQLDataType.smallInteger: - return "int2"; + return 'int2'; case PostgreSQLDataType.bigInteger: - return "int8"; + return 'int8'; case PostgreSQLDataType.serial: - return "int4"; + return 'int4'; case PostgreSQLDataType.bigSerial: - return "int8"; + return 'int8'; case PostgreSQLDataType.real: - return "float4"; + return 'float4'; case PostgreSQLDataType.double: - return "float8"; + return 'float8'; case PostgreSQLDataType.boolean: - return "boolean"; + return 'boolean'; case PostgreSQLDataType.timestampWithoutTimezone: - return "timestamp"; + return 'timestamp'; case PostgreSQLDataType.timestampWithTimezone: - return "timestamptz"; + return 'timestamptz'; case PostgreSQLDataType.date: - return "date"; + return 'date'; case PostgreSQLDataType.json: - return "jsonb"; + return 'jsonb'; case PostgreSQLDataType.byteArray: - return "bytea"; + return 'bytea'; case PostgreSQLDataType.name: - return "name"; + return 'name'; case PostgreSQLDataType.uuid: - return "uuid"; + return 'uuid'; } return null; } static String substitute(String fmtString, Map values, - {SQLReplaceIdentifierFunction replace: null}) { - final converter = new PostgresTextEncoder(true); + {SQLReplaceIdentifierFunction replace}) { + final converter = PostgresTextEncoder(true); values ??= {}; replace ??= (spec, index) => converter.convert(values[spec.name]); - var items = []; - PostgreSQLFormatToken currentPtr = null; - var iterator = new RuneIterator(fmtString); + final items = []; + PostgreSQLFormatToken currentPtr; + final iterator = RuneIterator(fmtString); iterator.moveNext(); while (iterator.current != null) { if (currentPtr == null) { - if (iterator.current == _AtSignCodeUnit) { + if (iterator.current == _atSignCodeUnit) { currentPtr = - new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } else { - currentPtr = - new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); + currentPtr = PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } } else if (currentPtr.type == PostgreSQLFormatTokenType.text) { - if (iterator.current == _AtSignCodeUnit) { + if (iterator.current == _atSignCodeUnit) { currentPtr = - new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } else { currentPtr.buffer.writeCharCode(iterator.current); } } else if (currentPtr.type == PostgreSQLFormatTokenType.variable) { - if (iterator.current == _AtSignCodeUnit) { + if (iterator.current == _atSignCodeUnit) { iterator.movePrevious(); - if (iterator.current == _AtSignCodeUnit) { + if (iterator.current == _atSignCodeUnit) { currentPtr.buffer.writeCharCode(iterator.current); currentPtr.type = PostgreSQLFormatTokenType.text; } else { currentPtr = - new PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); + PostgreSQLFormatToken(PostgreSQLFormatTokenType.variable); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } @@ -101,8 +100,7 @@ class PostgreSQLFormat { } else if (_isIdentifier(iterator.current)) { currentPtr.buffer.writeCharCode(iterator.current); } else { - currentPtr = - new PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); + currentPtr = PostgreSQLFormatToken(PostgreSQLFormatTokenType.text); currentPtr.buffer.writeCharCode(iterator.current); items.add(currentPtr); } @@ -118,40 +116,40 @@ class PostgreSQLFormat { } else if (t.buffer.length == 1 && t.buffer.toString() == '@') { return t.buffer; } else { - var identifier = new PostgreSQLFormatIdentifier(t.buffer.toString()); + final identifier = PostgreSQLFormatIdentifier(t.buffer.toString()); if (!values.containsKey(identifier.name)) { - throw new FormatException( - "Format string specified identifier with name ${identifier.name}, but key was not present in values. Format string: $fmtString"); + throw FormatException( + 'Format string specified identifier with name ${identifier.name}, but key was not present in values. Format string: $fmtString'); } - var val = replace(identifier, idx); + final val = replace(identifier, idx); idx++; if (identifier.typeCast != null) { - return val + "::" + identifier.typeCast; + return '$val::${identifier.typeCast}'; } return val; } - }).join(""); + }).join(''); } - static int _lowercaseACodeUnit = "a".codeUnitAt(0); - static int _uppercaseACodeUnit = "A".codeUnitAt(0); - static int _lowercaseZCodeUnit = "z".codeUnitAt(0); - static int _uppercaseZCodeUnit = "Z".codeUnitAt(0); - static int _0CodeUnit = "0".codeUnitAt(0); - static int _9CodeUnit = "9".codeUnitAt(0); - static int _underscoreCodeUnit = "_".codeUnitAt(0); - static int _ColonCodeUnit = ":".codeUnitAt(0); + static final int _lowercaseACodeUnit = 'a'.codeUnitAt(0); + static final int _uppercaseACodeUnit = 'A'.codeUnitAt(0); + static final int _lowercaseZCodeUnit = 'z'.codeUnitAt(0); + static final int _uppercaseZCodeUnit = 'Z'.codeUnitAt(0); + static final int _codeUnit0 = '0'.codeUnitAt(0); + static final int _codeUnit9 = '9'.codeUnitAt(0); + static final int _underscoreCodeUnit = '_'.codeUnitAt(0); + static final int _colonCodeUnit = ':'.codeUnitAt(0); static bool _isIdentifier(int charCode) { return (charCode >= _lowercaseACodeUnit && charCode <= _lowercaseZCodeUnit) || (charCode >= _uppercaseACodeUnit && charCode <= _uppercaseZCodeUnit) || - (charCode >= _0CodeUnit && charCode <= _9CodeUnit) || + (charCode >= _codeUnit0 && charCode <= _codeUnit9) || (charCode == _underscoreCodeUnit) || - (charCode == _ColonCodeUnit); + (charCode == _colonCodeUnit); } } diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index 92f914b..f85516e 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -10,7 +10,7 @@ class PostgresTextEncoder extends Converter { @override String convert(dynamic value) { if (value == null) { - return "null"; + return 'null'; } if (value is int) { @@ -37,7 +37,7 @@ class PostgresTextEncoder extends Converter { return encodeJSON(value); } - throw new PostgreSQLException("Could not infer type of value '$value'."); + throw PostgreSQLException("Could not infer type of value '$value'."); } String encodeString(String text, bool escapeStrings) { @@ -45,12 +45,12 @@ class PostgresTextEncoder extends Converter { return text; } - var backslashCodeUnit = r"\".codeUnitAt(0); - var quoteCodeUnit = r"'".codeUnitAt(0); + final backslashCodeUnit = r'\'.codeUnitAt(0); + final quoteCodeUnit = r"'".codeUnitAt(0); - var quoteCount = 0; - var backslashCount = 0; - var it = new RuneIterator(text); + int quoteCount = 0; + int backslashCount = 0; + final it = RuneIterator(text); while (it.moveNext()) { if (it.current == backslashCodeUnit) { backslashCount++; @@ -59,10 +59,10 @@ class PostgresTextEncoder extends Converter { } } - var buf = new StringBuffer(); + final buf = StringBuffer(); if (backslashCount > 0) { - buf.write(" E"); + buf.write(' E'); } buf.write("'"); @@ -110,37 +110,37 @@ class PostgresTextEncoder extends Converter { } String encodeBoolean(bool value) { - return value ? "TRUE" : "FALSE"; + return value ? 'TRUE' : 'FALSE'; } - String encodeDateTime(DateTime value, {bool isDateOnly: false}) { + String encodeDateTime(DateTime value, {bool isDateOnly}) { var string = value.toIso8601String(); if (isDateOnly) { - string = string.split("T").first; + string = string.split('T').first; } else { if (!value.isUtc) { - var timezoneHourOffset = value.timeZoneOffset.inHours; - var timezoneMinuteOffset = value.timeZoneOffset.inMinutes % 60; + final timezoneHourOffset = value.timeZoneOffset.inHours; + final timezoneMinuteOffset = value.timeZoneOffset.inMinutes % 60; - var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, "0"); - var minuteComponent = - timezoneMinuteOffset.abs().toString().padLeft(2, "0"); + var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, '0'); + final minuteComponent = + timezoneMinuteOffset.abs().toString().padLeft(2, '0'); if (timezoneHourOffset >= 0) { - hourComponent = "+${hourComponent}"; + hourComponent = '+$hourComponent'; } else { - hourComponent = "-${hourComponent}"; + hourComponent = '-$hourComponent'; } - var timezoneString = [hourComponent, minuteComponent].join(":"); - string = [string, timezoneString].join(""); + final timezoneString = [hourComponent, minuteComponent].join(':'); + string = [string, timezoneString].join(''); } } - if (string.substring(0, 1) == "-") { - string = string.substring(1) + " BC"; - } else if (string.substring(0, 1) == "+") { + if (string.substring(0, 1) == '-') { + string = '${string.substring(1)} BC'; + } else if (string.substring(0, 1) == '+') { string = string.substring(1); } @@ -149,13 +149,13 @@ class PostgresTextEncoder extends Converter { String encodeJSON(dynamic value) { if (value == null) { - return "null"; + return 'null'; } if (value is String) { return "'${json.encode(value)}'"; } - return "${json.encode(value)}"; + return json.encode(value); } } diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index a661afe..df530c3 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -8,23 +8,25 @@ class _TransactionProxy extends Object implements PostgreSQLExecutionContext { _TransactionProxy( this._connection, this.executionBlock, this.commitTimeoutInSeconds) { - beginQuery = new Query("BEGIN", {}, _connection, this) + beginQuery = Query('BEGIN', {}, _connection, this) ..onlyReturnAffectedRowCount = true; - beginQuery.future.then(startTransaction).catchError((err, st) { - new Future(() { + beginQuery.future.then(startTransaction).catchError((err, StackTrace st) { + Future(() { completer.completeError(err, st); }); }); } Query beginQuery; - Completer completer = new Completer(); + Completer completer = Completer(); Future get future => completer.future; + @override final PostgreSQLConnection _connection; + @override PostgreSQLExecutionContext get _transaction => this; final _TransactionQuerySignature executionBlock; @@ -32,18 +34,19 @@ class _TransactionProxy extends Object bool _hasFailed = false; bool _hasRolledBack = false; - void cancelTransaction({String reason: null}) { - throw new _TransactionRollbackException(reason); + @override + void cancelTransaction({String reason}) { + throw _TransactionRollbackException(reason); } Future startTransaction(dynamic _) async { - var result; + dynamic result; try { result = await executionBlock(this); // Place another event in the queue so that any non-awaited futures // in the executionBlock are given a chance to run - await new Future(() => null); + await Future(() => null); } on _TransactionRollbackException catch (rollback) { await _cancelAndRollback(rollback); @@ -62,7 +65,7 @@ class _TransactionProxy extends Object } if (!_hasRolledBack && !_hasFailed) { - await execute("COMMIT", timeoutInSeconds: commitTimeoutInSeconds); + await execute('COMMIT', timeoutInSeconds: commitTimeoutInSeconds); completer.complete(result); } } @@ -79,25 +82,25 @@ class _TransactionProxy extends Object q.future.catchError((_) {}); }); - final err = new PostgreSQLException("Query failed prior to execution. " + final err = PostgreSQLException('Query failed prior to execution. ' "This query's transaction encountered an error earlier in the transaction " - "that prevented this query from executing."); + 'that prevented this query from executing.'); _queue.cancel(err); - var rollback = new Query("ROLLBACK", {}, _connection, _transaction) + final rollback = Query('ROLLBACK', {}, _connection, _transaction) ..onlyReturnAffectedRowCount = true; _queue.addEvenIfCancelled(rollback); _connection._transitionToState(_connection._connectionState.awake()); try { - await rollback.future.timeout(new Duration(seconds: 30)); + await rollback.future.timeout(Duration(seconds: 30)); } finally { _queue.remove(rollback); } if (object is _TransactionRollbackException) { - completer.complete(new PostgreSQLRollback._(object.reason)); + completer.complete(PostgreSQLRollback._(object.reason)); } else { completer.completeError(object, trace); } diff --git a/lib/src/utf8_backed_string.dart b/lib/src/utf8_backed_string.dart index 08bac39..360344d 100644 --- a/lib/src/utf8_backed_string.dart +++ b/lib/src/utf8_backed_string.dart @@ -10,16 +10,12 @@ class UTF8BackedString { final String string; int get utf8Length { - if (_cachedUTF8Bytes == null) { - _cachedUTF8Bytes = utf8.encode(string); - } + _cachedUTF8Bytes ??= utf8.encode(string); return _cachedUTF8Bytes.length; } List get utf8Bytes { - if (_cachedUTF8Bytes == null) { - _cachedUTF8Bytes = utf8.encode(string); - } + _cachedUTF8Bytes ??= utf8.encode(string); return _cachedUTF8Bytes; } } diff --git a/pubspec.yaml b/pubspec.yaml index 326ae3b..3d853df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,12 +6,13 @@ homepage: https://github.com/stablekernel/postgresql-dart documentation: environment: - sdk: ">=2.0.0 <3.0.0" + sdk: ">=2.2.0 <3.0.0" dependencies: - buffer: ^1.0.5 + buffer: ^1.0.6 crypto: ^2.0.0 dev_dependencies: + pedantic: ^1.0.0 test: ^1.3.0 coverage: any diff --git a/test/connection_test.dart b/test/connection_test.dart index 9e6fd3c..b1c2b52 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -1,155 +1,158 @@ // ignore_for_file: unawaited_futures -import 'package:postgres/postgres.dart'; -import 'package:test/test.dart'; -import 'dart:io'; + import 'dart:async'; +import 'dart:io'; import 'dart:mirrors'; +import 'package:test/test.dart'; + +import 'package:postgres/postgres.dart'; + void main() { - group("Connection lifecycle", () { - PostgreSQLConnection conn = null; + group('Connection lifecycle', () { + PostgreSQLConnection conn; tearDown(() async { await conn?.close(); }); - test("Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + test('Connect with md5 auth required', () async { + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); - expect(await conn.execute("select 1"), equals(1)); + expect(await conn.execute('select 1'), equals(1)); }); - test("SSL Connect with md5 auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart", useSSL: true); + test('SSL Connect with md5 auth required', () async { + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart', useSSL: true); await conn.open(); - expect(await conn.execute("select 1"), equals(1)); - var socketMirror = reflect(conn).type.declarations.values.firstWhere( + expect(await conn.execute('select 1'), equals(1)); + final socketMirror = reflect(conn).type.declarations.values.firstWhere( (DeclarationMirror dm) => - dm.simpleName.toString().contains("_socket")); - var underlyingSocket = + dm.simpleName.toString().contains('_socket')); + final underlyingSocket = reflect(conn).getField(socketMirror.simpleName).reflectee; expect(underlyingSocket is SecureSocket, true); }); - test("Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test('Connect with no auth required', () async { + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); - expect(await conn.execute("select 1"), equals(1)); + expect(await conn.execute('select 1'), equals(1)); }); - test("SSL Connect with no auth required", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + test('SSL Connect with no auth required', () async { + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust', useSSL: true); await conn.open(); - expect(await conn.execute("select 1"), equals(1)); + expect(await conn.execute('select 1'), equals(1)); }); - test("Closing idle connection succeeds, closes underlying socket", + test('Closing idle connection succeeds, closes underlying socket', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); await conn.close(); - var socketMirror = reflect(conn).type.declarations.values.firstWhere( + final socketMirror = reflect(conn).type.declarations.values.firstWhere( (DeclarationMirror dm) => - dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = - reflect(conn).getField(socketMirror.simpleName).reflectee; + dm.simpleName.toString().contains('_socket')); + final underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee as Socket; expect(await underlyingSocket.done, isNotNull); conn = null; }); - test("SSL Closing idle connection succeeds, closes underlying socket", + test('SSL Closing idle connection succeeds, closes underlying socket', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust', useSSL: true); await conn.open(); await conn.close(); - var socketMirror = reflect(conn).type.declarations.values.firstWhere( + final socketMirror = reflect(conn).type.declarations.values.firstWhere( (DeclarationMirror dm) => - dm.simpleName.toString().contains("_socket")); - Socket underlyingSocket = - reflect(conn).getField(socketMirror.simpleName).reflectee; + dm.simpleName.toString().contains('_socket')); + final underlyingSocket = + reflect(conn).getField(socketMirror.simpleName).reflectee as Socket; expect(await underlyingSocket.done, isNotNull); conn = null; }); test( - "Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", + 'Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); - var errors = []; + final errors = []; final catcher = (e) { errors.add(e); return null; }; - var futures = [ - conn.query("select 1", allowReuse: false).catchError(catcher), - conn.query("select 2", allowReuse: false).catchError(catcher), - conn.query("select 3", allowReuse: false).catchError(catcher), - conn.query("select 4", allowReuse: false).catchError(catcher), - conn.query("select 5", allowReuse: false).catchError(catcher), + final futures = [ + conn.query('select 1', allowReuse: false).catchError(catcher), + conn.query('select 2', allowReuse: false).catchError(catcher), + conn.query('select 3', allowReuse: false).catchError(catcher), + conn.query('select 4', allowReuse: false).catchError(catcher), + conn.query('select 5', allowReuse: false).catchError(catcher), ]; await conn.close(); await Future.wait(futures); expect(errors.length, 5); expect(errors.map((e) => e.message), - everyElement(contains("Query cancelled"))); + everyElement(contains('Query cancelled'))); }); test( - "SSL Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket", + 'SSL Closing connection while busy succeeds, queued queries are all accounted for (canceled), closes underlying socket', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust', useSSL: true); await conn.open(); - var errors = []; + final errors = []; final catcher = (e) { errors.add(e); return null; }; - var futures = [ - conn.query("select 1", allowReuse: false).catchError(catcher), - conn.query("select 2", allowReuse: false).catchError(catcher), - conn.query("select 3", allowReuse: false).catchError(catcher), - conn.query("select 4", allowReuse: false).catchError(catcher), - conn.query("select 5", allowReuse: false).catchError(catcher), + final futures = [ + conn.query('select 1', allowReuse: false).catchError(catcher), + conn.query('select 2', allowReuse: false).catchError(catcher), + conn.query('select 3', allowReuse: false).catchError(catcher), + conn.query('select 4', allowReuse: false).catchError(catcher), + conn.query('select 5', allowReuse: false).catchError(catcher), ]; await conn.close(); await Future.wait(futures); expect(errors.length, 5); expect(errors.map((e) => e.message), - everyElement(contains("Query cancelled"))); + everyElement(contains('Query cancelled'))); }); }); - group("Successful queries over time", () { - PostgreSQLConnection conn = null; + group('Successful queries over time', () { + PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); }); @@ -158,47 +161,47 @@ void main() { }); test( - "Issuing multiple queries and awaiting between each one successfully returns the right value", + 'Issuing multiple queries and awaiting between each one successfully returns the right value', () async { expect( - await conn.query("select 1", allowReuse: false), + await conn.query('select 1', allowReuse: false), equals([ [1] ])); expect( - await conn.query("select 2", allowReuse: false), + await conn.query('select 2', allowReuse: false), equals([ [2] ])); expect( - await conn.query("select 3", allowReuse: false), + await conn.query('select 3', allowReuse: false), equals([ [3] ])); expect( - await conn.query("select 4", allowReuse: false), + await conn.query('select 4', allowReuse: false), equals([ [4] ])); expect( - await conn.query("select 5", allowReuse: false), + await conn.query('select 5', allowReuse: false), equals([ [5] ])); }); test( - "Issuing multiple queries without awaiting are returned with appropriate values", + 'Issuing multiple queries without awaiting are returned with appropriate values', () async { - var futures = [ - conn.query("select 1", allowReuse: false), - conn.query("select 2", allowReuse: false), - conn.query("select 3", allowReuse: false), - conn.query("select 4", allowReuse: false), - conn.query("select 5", allowReuse: false) + final futures = [ + conn.query('select 1', allowReuse: false), + conn.query('select 2', allowReuse: false), + conn.query('select 3', allowReuse: false), + conn.query('select 4', allowReuse: false), + conn.query('select 5', allowReuse: false) ]; - var results = await Future.wait(futures); + final results = await Future.wait(futures); expect(results, [ [ @@ -220,8 +223,8 @@ void main() { }); }); - group("Unintended user-error situations", () { - PostgreSQLConnection conn = null; + group('Unintended user-error situations', () { + PostgreSQLConnection conn; Future openFuture; tearDown(() async { @@ -229,133 +232,133 @@ void main() { await conn?.close(); }); - test("Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + test('Sending queries to opening connection triggers error', () async { + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); openFuture = conn.open(); try { - await conn.execute("select 1"); + await conn.execute('select 1'); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("connection is not open")); + expect(e.message, contains('connection is not open')); } }); - test("SSL Sending queries to opening connection triggers error", () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + test('SSL Sending queries to opening connection triggers error', () async { + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust', useSSL: true); openFuture = conn.open(); try { - await conn.execute("select 1"); + await conn.execute('select 1'); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("connection is not open")); + expect(e.message, contains('connection is not open')); } }); - test("Starting transaction while opening connection triggers error", + test('Starting transaction while opening connection triggers error', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); openFuture = conn.open(); try { await conn.transaction((ctx) async { - await ctx.execute("select 1"); + await ctx.execute('select 1'); }); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("connection is not open")); + expect(e.message, contains('connection is not open')); } }); - test("SSL Starting transaction while opening connection triggers error", + test('SSL Starting transaction while opening connection triggers error', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust", useSSL: true); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust', useSSL: true); openFuture = conn.open(); try { await conn.transaction((ctx) async { - await ctx.execute("select 1"); + await ctx.execute('select 1'); }); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("connection is not open")); + expect(e.message, contains('connection is not open')); } }); - test("Invalid password reports error, conn is closed, disables conn", + test('Invalid password reports error, conn is closed, disables conn', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "notdart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'notdart'); try { await conn.open(); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("password authentication failed")); + expect(e.message, contains('password authentication failed')); } await expectConnectionIsInvalid(conn); }); - test("SSL Invalid password reports error, conn is closed, disables conn", + test('SSL Invalid password reports error, conn is closed, disables conn', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "notdart", useSSL: true); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'notdart', useSSL: true); try { await conn.open(); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("password authentication failed")); + expect(e.message, contains('password authentication failed')); } await expectConnectionIsInvalid(conn); }); - test("A query error maintains connectivity, allows future queries", + test('A query error maintains connectivity, allows future queries', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); - await conn.execute("INSERT INTO t (i) VALUES (1)"); + await conn.execute('CREATE TEMPORARY TABLE t (i int unique)'); + await conn.execute('INSERT INTO t (i) VALUES (1)'); try { - await conn.execute("INSERT INTO t (i) VALUES (1)"); + await conn.execute('INSERT INTO t (i) VALUES (1)'); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("duplicate key value violates")); + expect(e.message, contains('duplicate key value violates')); } - await conn.execute("INSERT INTO t (i) VALUES (2)"); + await conn.execute('INSERT INTO t (i) VALUES (2)'); }); test( - "A query error maintains connectivity, continues processing pending queries", + 'A query error maintains connectivity, continues processing pending queries', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); + await conn.execute('CREATE TEMPORARY TABLE t (i int unique)'); - await conn.execute("INSERT INTO t (i) VALUES (1)"); + await conn.execute('INSERT INTO t (i) VALUES (1)'); //ignore: unawaited_futures - conn.execute("INSERT INTO t (i) VALUES (1)").catchError((err) { + conn.execute('INSERT INTO t (i) VALUES (1)').catchError((err) { // ignore }); - var futures = [ - conn.query("select 1", allowReuse: false), - conn.query("select 2", allowReuse: false), - conn.query("select 3", allowReuse: false), + final futures = [ + conn.query('select 1', allowReuse: false), + conn.query('select 2', allowReuse: false), + conn.query('select 3', allowReuse: false), ]; - var results = await Future.wait(futures); + final results = await Future.wait(futures); expect(results, [ [ @@ -369,37 +372,37 @@ void main() { ] ]); - var queueMirror = reflect(conn).type.instanceMembers.values.firstWhere( + final queueMirror = reflect(conn).type.instanceMembers.values.firstWhere( (DeclarationMirror dm) => - dm.simpleName.toString().contains("_queue")); - List queue = - reflect(conn).getField(queueMirror.simpleName).reflectee; + dm.simpleName.toString().contains('_queue')); + final queue = + reflect(conn).getField(queueMirror.simpleName).reflectee as List; expect(queue, isEmpty); }); test( - "A query error maintains connectivity, continues processing pending transactions", + 'A query error maintains connectivity, continues processing pending transactions', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (i int unique)"); - await conn.execute("INSERT INTO t (i) VALUES (1)"); + await conn.execute('CREATE TEMPORARY TABLE t (i int unique)'); + await conn.execute('INSERT INTO t (i) VALUES (1)'); final orderEnsurer = []; // this will emit a query error //ignore: unawaited_futures - conn.execute("INSERT INTO t (i) VALUES (1)").catchError((err) { + conn.execute('INSERT INTO t (i) VALUES (1)').catchError((err) { orderEnsurer.add(1); // ignore }); orderEnsurer.add(2); - var res = await conn.transaction((ctx) async { + final res = await conn.transaction((ctx) async { orderEnsurer.add(3); - return await ctx.query("SELECT i FROM t"); + return await ctx.query('SELECT i FROM t'); }); orderEnsurer.add(4); @@ -410,22 +413,22 @@ void main() { }); test( - "Building query throws error, connection continues processing pending queries", + 'Building query throws error, connection continues processing pending queries', () async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "darttrust"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'darttrust'); await conn.open(); // Make some async queries that'll exit the event loop, but then fail on a query that'll die early - conn.execute("askdl").catchError((err, st) {}); - conn.execute("abdef").catchError((err, st) {}); - conn.execute("select @a").catchError((err, st) {}); + conn.execute('askdl').catchError((err, st) {}); + conn.execute('abdef').catchError((err, st) {}); + conn.execute('select @a').catchError((err, st) {}); - var futures = [ - conn.query("select 1", allowReuse: false), - conn.query("select 2", allowReuse: false), + final futures = [ + conn.query('select 1', allowReuse: false), + conn.query('select 2', allowReuse: false), ]; - var results = await Future.wait(futures); + final results = await Future.wait(futures); expect(results, [ [ @@ -436,18 +439,18 @@ void main() { ] ]); - var queueMirror = reflect(conn).type.instanceMembers.values.firstWhere( + final queueMirror = reflect(conn).type.instanceMembers.values.firstWhere( (DeclarationMirror dm) => - dm.simpleName.toString().contains("_queue")); - List queue = - reflect(conn).getField(queueMirror.simpleName).reflectee; + dm.simpleName.toString().contains('_queue')); + final queue = + reflect(conn).getField(queueMirror.simpleName).reflectee as List; expect(queue, isEmpty); }); }); - group("Network error situations", () { - ServerSocket serverSocket = null; - Socket socket = null; + group('Network error situations', () { + ServerSocket serverSocket; + Socket socket; tearDown(() async { await serverSocket?.close(); @@ -455,9 +458,9 @@ void main() { }); test( - "Socket fails to connect reports error, disables connection for future use", + 'Socket fails to connect reports error, disables connection for future use', () async { - var conn = new PostgreSQLConnection("localhost", 5431, "dart_test"); + final conn = PostgreSQLConnection('localhost', 5431, 'dart_test'); try { await conn.open(); @@ -468,10 +471,10 @@ void main() { }); test( - "SSL Socket fails to connect reports error, disables connection for future use", + 'SSL Socket fails to connect reports error, disables connection for future use', () async { - var conn = new PostgreSQLConnection("localhost", 5431, "dart_test", - useSSL: true); + final conn = + PostgreSQLConnection('localhost', 5431, 'dart_test', useSSL: true); try { await conn.open(); @@ -482,7 +485,7 @@ void main() { }); test( - "Connection that times out throws appropriate error and cannot be reused", + 'Connection that times out throws appropriate error and cannot be reused', () async { serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); @@ -492,7 +495,7 @@ void main() { s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + final conn = PostgreSQLConnection('localhost', 5433, 'dart_test', timeoutInSeconds: 2); try { @@ -504,7 +507,7 @@ void main() { }); test( - "SSL Connection that times out throws appropriate error and cannot be reused", + 'SSL Connection that times out throws appropriate error and cannot be reused', () async { serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); @@ -514,7 +517,7 @@ void main() { s.listen((bytes) {}); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + final conn = PostgreSQLConnection('localhost', 5433, 'dart_test', timeoutInSeconds: 2, useSSL: true); try { @@ -525,49 +528,45 @@ void main() { await expectConnectionIsInvalid(conn); }); - test("Connection that times out triggers future for pending queries", + test('Connection that times out triggers future for pending queries', () async { - var openCompleter = new Completer(); + final openCompleter = Completer(); serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); - new Future.delayed(new Duration(milliseconds: 100), () { - openCompleter.complete(); - }); + Future.delayed(Duration(milliseconds: 100), openCompleter.complete); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + final conn = PostgreSQLConnection('localhost', 5433, 'dart_test', timeoutInSeconds: 2); conn.open().catchError((e) {}); await openCompleter.future; try { - await conn.execute("select 1"); + await conn.execute('select 1'); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("Failed to connect")); + expect(e.message, contains('Failed to connect')); } }); - test("SSL Connection that times out triggers future for pending queries", + test('SSL Connection that times out triggers future for pending queries', () async { - var openCompleter = new Completer(); + final openCompleter = Completer(); serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 5433); serverSocket.listen((s) { socket = s; // Don't respond on purpose s.listen((bytes) {}); - new Future.delayed(new Duration(milliseconds: 100), () { - openCompleter.complete(); - }); + Future.delayed(Duration(milliseconds: 100), openCompleter.complete); }); - var conn = new PostgreSQLConnection("localhost", 5433, "dart_test", + final conn = PostgreSQLConnection('localhost', 5433, 'dart_test', timeoutInSeconds: 2, useSSL: true); conn.open().catchError((e) { return null; @@ -576,10 +575,10 @@ void main() { await openCompleter.future; try { - await conn.execute("select 1"); + await conn.execute('select 1'); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("but connection is not open")); + expect(e.message, contains('but connection is not open')); } try { @@ -589,51 +588,51 @@ void main() { }); }); - test("If connection is closed, do not allow .execute", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + test('If connection is closed, do not allow .execute', () async { + final conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); try { - await conn.execute("SELECT 1"); + await conn.execute('SELECT 1'); fail('unreachable'); } on PostgreSQLException catch (e) { - expect(e.toString(), contains("connection is not open")); + expect(e.toString(), contains('connection is not open')); } }); - test("If connection is closed, do not allow .query", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + test('If connection is closed, do not allow .query', () async { + final conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); try { - await conn.query("SELECT 1"); + await conn.query('SELECT 1'); fail('unreachable'); } on PostgreSQLException catch (e) { - expect(e.toString(), contains("connection is not open")); + expect(e.toString(), contains('connection is not open')); } }); - test("If connection is closed, do not allow .mappedResultsQuery", () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + test('If connection is closed, do not allow .mappedResultsQuery', () async { + final conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); try { - await conn.mappedResultsQuery("SELECT 1"); + await conn.mappedResultsQuery('SELECT 1'); fail('unreachable'); } on PostgreSQLException catch (e) { - expect(e.toString(), contains("connection is not open")); + expect(e.toString(), contains('connection is not open')); } }); test( - "Queue size, should be 0 on open, >0 if queries added and 0 again after queries executed", + 'Queue size, should be 0 on open, >0 if queries added and 0 again after queries executed', () async { - final conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + final conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); expect(conn.queueSize, 0); - var futures = [ - conn.query("select 1", allowReuse: false), - conn.query("select 2", allowReuse: false), - conn.query("select 3", allowReuse: false) + final futures = [ + conn.query('select 1', allowReuse: false), + conn.query('select 2', allowReuse: false), + conn.query('select 3', allowReuse: false) ]; expect(conn.queueSize, 3); @@ -644,16 +643,16 @@ void main() { Future expectConnectionIsInvalid(PostgreSQLConnection conn) async { try { - await conn.execute("select 1"); + await conn.execute('select 1'); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("connection is not open")); + expect(e.message, contains('connection is not open')); } try { await conn.open(); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("Attempting to reopen a closed connection")); + expect(e.message, contains('Attempting to reopen a closed connection')); } } diff --git a/test/decode_test.dart b/test/decode_test.dart index 7d255fc..27096fb 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -4,45 +4,45 @@ import 'package:test/test.dart'; void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); - await connection.execute(""" + await connection.execute(''' CREATE TEMPORARY TABLE t ( i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb, ba bytea, u uuid) - """); + '''); await connection.execute( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " - "VALUES (-2147483648, -9223372036854775808, TRUE, -32768, " + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) ' + 'VALUES (-2147483648, -9223372036854775808, TRUE, -32768, ' "'string', 10.0, 10.0, '1983-11-06', " "'1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000', " "'{\"key\":\"value\"}', E'\\\\000', '00000000-0000-0000-0000-000000000000')"); await connection.execute( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " - "VALUES (2147483647, 9223372036854775807, FALSE, 32767, " + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) ' + 'VALUES (2147483647, 9223372036854775807, FALSE, 32767, ' "'a significantly longer string to the point where i doubt this actually matters', " "10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', " "'2183-11-06 00:00:00.999999', " "'[{\"key\":1}]', E'\\\\377', 'FFFFFFFF-ffff-ffff-ffff-ffffffffffff')"); await connection.execute( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) " - "VALUES (null, null, null, null, null, null, null, null, null, null, null, null, null)"); + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u) ' + 'VALUES (null, null, null, null, null, null, null, null, null, null, null, null, null)'); }); tearDown(() async { await connection?.close(); }); - test("Fetch em", () async { - var res = await connection.query("select * from t"); + test('Fetch em', () async { + final res = await connection.query('select * from t'); - var row1 = res[0]; - var row2 = res[1]; - var row3 = res[2]; + final row1 = res[0]; + final row2 = res[1]; + final row3 = res[2]; // lower bound row expect(row1[0], equals(-2147483648)); @@ -51,17 +51,17 @@ void main() { expect(row1[3], equals(1)); expect(row1[4], equals(true)); expect(row1[5], equals(-32768)); - expect(row1[6], equals("string")); + expect(row1[6], equals('string')); expect(row1[7] is double, true); expect(row1[7], equals(10.0)); expect(row1[8] is double, true); expect(row1[8], equals(10.0)); - expect(row1[9], equals(new DateTime.utc(1983, 11, 6))); - expect(row1[10], equals(new DateTime.utc(1983, 11, 6, 6))); - expect(row1[11], equals(new DateTime.utc(1983, 11, 6, 6))); - expect(row1[12], equals({"key": "value"})); + expect(row1[9], equals(DateTime.utc(1983, 11, 6))); + expect(row1[10], equals(DateTime.utc(1983, 11, 6, 6))); + expect(row1[11], equals(DateTime.utc(1983, 11, 6, 6))); + expect(row1[12], equals({'key': 'value'})); expect(row1[13], equals([0])); - expect(row1[14], equals("00000000-0000-0000-0000-000000000000")); + expect(row1[14], equals('00000000-0000-0000-0000-000000000000')); // upper bound row expect(row2[0], equals(2147483647)); @@ -73,21 +73,21 @@ void main() { expect( row2[6], equals( - "a significantly longer string to the point where i doubt this actually matters")); + 'a significantly longer string to the point where i doubt this actually matters')); expect(row2[7] is double, true); expect(row2[7], equals(10.25)); expect(row2[8] is double, true); expect(row2[8], equals(10.125)); - expect(row2[9], equals(new DateTime.utc(2183, 11, 6))); - expect(row2[10], equals(new DateTime.utc(2183, 11, 6, 0, 0, 0, 111, 111))); - expect(row2[11], equals(new DateTime.utc(2183, 11, 6, 0, 0, 0, 999, 999))); + expect(row2[9], equals(DateTime.utc(2183, 11, 6))); + expect(row2[10], equals(DateTime.utc(2183, 11, 6, 0, 0, 0, 111, 111))); + expect(row2[11], equals(DateTime.utc(2183, 11, 6, 0, 0, 0, 999, 999))); expect( row2[12], equals([ - {"key": 1} + {'key': 1} ])); expect(row2[13], equals([255])); - expect(row2[14], equals("ffffffff-ffff-ffff-ffff-ffffffffffff")); + expect(row2[14], equals('ffffffff-ffff-ffff-ffff-ffffffffffff')); // all null row expect(row3[0], isNull); @@ -107,31 +107,31 @@ void main() { expect(row3[14], isNull); }); - test("Fetch/insert empty string", () async { - await connection.execute("CREATE TEMPORARY TABLE u (t text)"); + test('Fetch/insert empty string', () async { + await connection.execute('CREATE TEMPORARY TABLE u (t text)'); var results = await connection.query( - "INSERT INTO u (t) VALUES (@t:text) returning t", - substitutionValues: {"t": ""}); + 'INSERT INTO u (t) VALUES (@t:text) returning t', + substitutionValues: {'t': ''}); expect(results, [ - [""] + [''] ]); - results = await connection.query("select * from u"); + results = await connection.query('select * from u'); expect(results, [ - [""] + [''] ]); }); - test("Fetch/insert null value", () async { - await connection.execute("CREATE TEMPORARY TABLE u (t text)"); + test('Fetch/insert null value', () async { + await connection.execute('CREATE TEMPORARY TABLE u (t text)'); var results = await connection.query( - "INSERT INTO u (t) VALUES (@t:text) returning t", - substitutionValues: {"t": null}); + 'INSERT INTO u (t) VALUES (@t:text) returning t', + substitutionValues: {'t': null}); expect(results, [ [null] ]); - results = await connection.query("select * from u"); + results = await connection.query('select * from u'); expect(results, [ [null] ]); diff --git a/test/encoding_test.dart b/test/encoding_test.dart index d736d12..877f1cf 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -12,10 +12,10 @@ import 'package:postgres/src/utf8_backed_string.dart'; PostgreSQLConnection conn; void main() { - group("Binary encoders", () { + group('Binary encoders', () { setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); }); @@ -28,215 +28,212 @@ void main() { // 1. encoder/decoder is reversible // 2. can actually encode and decode a real pg query // it also creates a table named t with column v of type being tested - test("bool", () async { + test('bool', () async { await expectInverse(true, PostgreSQLDataType.boolean); await expectInverse(false, PostgreSQLDataType.boolean); try { - await conn.query("INSERT INTO t (v) VALUES (@v:boolean)", - substitutionValues: {"v": "not-bool"}); + await conn.query('INSERT INTO t (v) VALUES (@v:boolean)', + substitutionValues: {'v': 'not-bool'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: bool")); + expect(e.toString(), contains('Expected: bool')); } }); - test("smallint", () async { + test('smallint', () async { await expectInverse(-1, PostgreSQLDataType.smallInteger); await expectInverse(0, PostgreSQLDataType.smallInteger); await expectInverse(1, PostgreSQLDataType.smallInteger); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int2)", - substitutionValues: {"v": "not-int2"}); + await conn.query('INSERT INTO t (v) VALUES (@v:int2)', + substitutionValues: {'v': 'not-int2'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: int")); + expect(e.toString(), contains('Expected: int')); } }); - test("integer", () async { + test('integer', () async { await expectInverse(-1, PostgreSQLDataType.integer); await expectInverse(0, PostgreSQLDataType.integer); await expectInverse(1, PostgreSQLDataType.integer); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int4)", - substitutionValues: {"v": "not-int4"}); + await conn.query('INSERT INTO t (v) VALUES (@v:int4)', + substitutionValues: {'v': 'not-int4'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: int")); + expect(e.toString(), contains('Expected: int')); } }); - test("serial", () async { + test('serial', () async { await expectInverse(0, PostgreSQLDataType.serial); await expectInverse(1, PostgreSQLDataType.serial); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int4)", - substitutionValues: {"v": "not-serial"}); + await conn.query('INSERT INTO t (v) VALUES (@v:int4)', + substitutionValues: {'v': 'not-serial'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: int")); + expect(e.toString(), contains('Expected: int')); } }); - test("bigint", () async { + test('bigint', () async { await expectInverse(-1, PostgreSQLDataType.bigInteger); await expectInverse(0, PostgreSQLDataType.bigInteger); await expectInverse(1, PostgreSQLDataType.bigInteger); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int8)", - substitutionValues: {"v": "not-int8"}); + await conn.query('INSERT INTO t (v) VALUES (@v:int8)', + substitutionValues: {'v': 'not-int8'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: int")); + expect(e.toString(), contains('Expected: int')); } }); - test("bigserial", () async { + test('bigserial', () async { await expectInverse(0, PostgreSQLDataType.bigSerial); await expectInverse(1, PostgreSQLDataType.bigSerial); try { - await conn.query("INSERT INTO t (v) VALUES (@v:int8)", - substitutionValues: {"v": "not-bigserial"}); + await conn.query('INSERT INTO t (v) VALUES (@v:int8)', + substitutionValues: {'v': 'not-bigserial'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: int")); + expect(e.toString(), contains('Expected: int')); } }); - test("text", () async { - await expectInverse("", PostgreSQLDataType.text); - await expectInverse("foo", PostgreSQLDataType.text); - await expectInverse("foo\n", PostgreSQLDataType.text); - await expectInverse("foo\nbar;s", PostgreSQLDataType.text); + test('text', () async { + await expectInverse('', PostgreSQLDataType.text); + await expectInverse('foo', PostgreSQLDataType.text); + await expectInverse('foo\n', PostgreSQLDataType.text); + await expectInverse('foo\nbar;s', PostgreSQLDataType.text); try { - await conn.query("INSERT INTO t (v) VALUES (@v:text)", - substitutionValues: {"v": 0}); + await conn.query('INSERT INTO t (v) VALUES (@v:text)', + substitutionValues: {'v': 0}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: String")); + expect(e.toString(), contains('Expected: String')); } }); - test("real", () async { + test('real', () async { await expectInverse(-1.0, PostgreSQLDataType.real); await expectInverse(0.0, PostgreSQLDataType.real); await expectInverse(1.0, PostgreSQLDataType.real); try { - await conn.query("INSERT INTO t (v) VALUES (@v:float4)", - substitutionValues: {"v": "not-real"}); + await conn.query('INSERT INTO t (v) VALUES (@v:float4)', + substitutionValues: {'v': 'not-real'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: double")); + expect(e.toString(), contains('Expected: double')); } }); - test("double", () async { + test('double', () async { await expectInverse(-1.0, PostgreSQLDataType.double); await expectInverse(0.0, PostgreSQLDataType.double); await expectInverse(1.0, PostgreSQLDataType.double); try { - await conn.query("INSERT INTO t (v) VALUES (@v:float8)", - substitutionValues: {"v": "not-double"}); + await conn.query('INSERT INTO t (v) VALUES (@v:float8)', + substitutionValues: {'v': 'not-double'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: double")); + expect(e.toString(), contains('Expected: double')); } }); - test("date", () async { - await expectInverse( - new DateTime.utc(1920, 10, 1), PostgreSQLDataType.date); - await expectInverse( - new DateTime.utc(2120, 10, 5), PostgreSQLDataType.date); - await expectInverse( - new DateTime.utc(2016, 10, 1), PostgreSQLDataType.date); + test('date', () async { + await expectInverse(DateTime.utc(1920, 10, 1), PostgreSQLDataType.date); + await expectInverse(DateTime.utc(2120, 10, 5), PostgreSQLDataType.date); + await expectInverse(DateTime.utc(2016, 10, 1), PostgreSQLDataType.date); try { - await conn.query("INSERT INTO t (v) VALUES (@v:date)", - substitutionValues: {"v": "not-date"}); + await conn.query('INSERT INTO t (v) VALUES (@v:date)', + substitutionValues: {'v': 'not-date'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: DateTime")); + expect(e.toString(), contains('Expected: DateTime')); } }); - test("timestamp", () async { - await expectInverse(new DateTime.utc(1920, 10, 1), + test('timestamp', () async { + await expectInverse(DateTime.utc(1920, 10, 1), PostgreSQLDataType.timestampWithoutTimezone); - await expectInverse(new DateTime.utc(2120, 10, 5), + await expectInverse(DateTime.utc(2120, 10, 5), PostgreSQLDataType.timestampWithoutTimezone); try { - await conn.query("INSERT INTO t (v) VALUES (@v:timestamp)", - substitutionValues: {"v": "not-timestamp"}); + await conn.query('INSERT INTO t (v) VALUES (@v:timestamp)', + substitutionValues: {'v': 'not-timestamp'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: DateTime")); + expect(e.toString(), contains('Expected: DateTime')); } }); - test("timestamptz", () async { - await expectInverse(new DateTime.utc(1920, 10, 1), - PostgreSQLDataType.timestampWithTimezone); - await expectInverse(new DateTime.utc(2120, 10, 5), - PostgreSQLDataType.timestampWithTimezone); + test('timestamptz', () async { + await expectInverse( + DateTime.utc(1920, 10, 1), PostgreSQLDataType.timestampWithTimezone); + await expectInverse( + DateTime.utc(2120, 10, 5), PostgreSQLDataType.timestampWithTimezone); try { - await conn.query("INSERT INTO t (v) VALUES (@v:timestamptz)", - substitutionValues: {"v": "not-timestamptz"}); + await conn.query('INSERT INTO t (v) VALUES (@v:timestamptz)', + substitutionValues: {'v': 'not-timestamptz'}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: DateTime")); + expect(e.toString(), contains('Expected: DateTime')); } }); - test("jsonb", () async { - await expectInverse("string", PostgreSQLDataType.json); + test('jsonb', () async { + await expectInverse('string', PostgreSQLDataType.json); await expectInverse(2, PostgreSQLDataType.json); - await expectInverse(["foo"], PostgreSQLDataType.json); + await expectInverse(['foo'], PostgreSQLDataType.json); await expectInverse({ - "key": "val", - "key1": 1, - "array": ["foo"] + 'key': 'val', + 'key1': 1, + 'array': ['foo'] }, PostgreSQLDataType.json); try { - await conn.query("INSERT INTO t (v) VALUES (@v:jsonb)", - substitutionValues: {"v": new DateTime.now()}); + await conn.query('INSERT INTO t (v) VALUES (@v:jsonb)', + substitutionValues: {'v': DateTime.now()}); fail('unreachable'); } on JsonUnsupportedObjectError catch (_) {} }); - test("bytea", () async { + test('bytea', () async { await expectInverse([0], PostgreSQLDataType.byteArray); await expectInverse([1, 2, 3, 4, 5], PostgreSQLDataType.byteArray); await expectInverse([255, 254, 253], PostgreSQLDataType.byteArray); try { - await conn.query("INSERT INTO t (v) VALUES (@v:bytea)", - substitutionValues: {"v": new DateTime.now()}); + await conn.query('INSERT INTO t (v) VALUES (@v:bytea)', + substitutionValues: {'v': DateTime.now()}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: List")); + expect(e.toString(), contains('Expected: List')); } }); - test("uuid", () async { + test('uuid', () async { await expectInverse( - "00000000-0000-0000-0000-000000000000", PostgreSQLDataType.uuid); + '00000000-0000-0000-0000-000000000000', PostgreSQLDataType.uuid); await expectInverse( - "12345678-abcd-efab-cdef-012345678901", PostgreSQLDataType.uuid); + '12345678-abcd-efab-cdef-012345678901', PostgreSQLDataType.uuid); try { - await conn.query("INSERT INTO t (v) VALUES (@v:uuid)", - substitutionValues: {"v": new DateTime.now()}); + await conn.query('INSERT INTO t (v) VALUES (@v:uuid)', + substitutionValues: {'v': DateTime.now()}); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Expected: String")); + expect(e.toString(), contains('Expected: String')); } }); }); - group("Text encoders", () { - test("Escape strings", () { - final encoder = new PostgresTextEncoder(true); + group('Text encoders', () { + test('Escape strings', () { + final encoder = PostgresTextEncoder(true); // ' b o b ' expect( utf8.encode(encoder.convert('bob')), equals([39, 98, 111, 98, 39])); @@ -273,91 +270,91 @@ void main() { equals([32, 69, 39, 92, 92, 39, 39, 39])); }); - test("Encode DateTime", () { + test('Encode DateTime', () { // Get users current timezone - var tz = new DateTime(2001, 2, 3).timeZoneOffset; - var tzOffsetDelimiter = "${tz.isNegative ? '-' : '+'}" - "${tz.abs().inHours.toString().padLeft(2, '0')}" - ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; - - var pairs = { - "2001-02-03T00:00:00.000$tzOffsetDelimiter": - new DateTime(2001, DateTime.february, 3), - "2001-02-03T04:05:06.000$tzOffsetDelimiter": - new DateTime(2001, DateTime.february, 3, 4, 5, 6, 0), - "2001-02-03T04:05:06.999$tzOffsetDelimiter": - new DateTime(2001, DateTime.february, 3, 4, 5, 6, 999), - "0010-02-03T04:05:06.123$tzOffsetDelimiter BC": - new DateTime(-10, DateTime.february, 3, 4, 5, 6, 123), - "0010-02-03T04:05:06.000$tzOffsetDelimiter BC": - new DateTime(-10, DateTime.february, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter BC": - new DateTime(-12345, DateTime.february, 3, 4, 5, 6, 0), - "012345-02-03T04:05:06.000$tzOffsetDelimiter": - new DateTime(12345, DateTime.february, 3, 4, 5, 6, 0) + final tz = DateTime(2001, 2, 3).timeZoneOffset; + final tzOffsetDelimiter = '${tz.isNegative ? '-' : '+'}' + '${tz.abs().inHours.toString().padLeft(2, '0')}' + ':${(tz.inSeconds % 60).toString().padLeft(2, '0')}'; + + final pairs = { + '2001-02-03T00:00:00.000$tzOffsetDelimiter': + DateTime(2001, DateTime.february, 3), + '2001-02-03T04:05:06.000$tzOffsetDelimiter': + DateTime(2001, DateTime.february, 3, 4, 5, 6, 0), + '2001-02-03T04:05:06.999$tzOffsetDelimiter': + DateTime(2001, DateTime.february, 3, 4, 5, 6, 999), + '0010-02-03T04:05:06.123$tzOffsetDelimiter BC': + DateTime(-10, DateTime.february, 3, 4, 5, 6, 123), + '0010-02-03T04:05:06.000$tzOffsetDelimiter BC': + DateTime(-10, DateTime.february, 3, 4, 5, 6, 0), + '012345-02-03T04:05:06.000$tzOffsetDelimiter BC': + DateTime(-12345, DateTime.february, 3, 4, 5, 6, 0), + '012345-02-03T04:05:06.000$tzOffsetDelimiter': + DateTime(12345, DateTime.february, 3, 4, 5, 6, 0) }; - final encoder = new PostgresTextEncoder(false); + final encoder = PostgresTextEncoder(false); pairs.forEach((k, v) { expect(encoder.convert(v), "'$k'"); }); }); - test("Encode Double", () { - var pairs = { + test('Encode Double', () { + final pairs = { "'nan'": double.nan, "'infinity'": double.infinity, "'-infinity'": double.negativeInfinity, - "1.7976931348623157e+308": double.maxFinite, - "5e-324": double.minPositive, - "-0.0": -0.0, - "0.0": 0.0 + '1.7976931348623157e+308': double.maxFinite, + '5e-324': double.minPositive, + '-0.0': -0.0, + '0.0': 0.0 }; - final encoder = new PostgresTextEncoder(false); + final encoder = PostgresTextEncoder(false); pairs.forEach((k, v) { - expect(encoder.convert(v), "$k"); + expect(encoder.convert(v), '$k'); }); }); - test("Encode Int", () { - final encoder = new PostgresTextEncoder(false); + test('Encode Int', () { + final encoder = PostgresTextEncoder(false); - expect(encoder.convert(1), "1"); - expect(encoder.convert(1234324323), "1234324323"); - expect(encoder.convert(-1234324323), "-1234324323"); + expect(encoder.convert(1), '1'); + expect(encoder.convert(1234324323), '1234324323'); + expect(encoder.convert(-1234324323), '-1234324323'); }); - test("Encode Bool", () { - final encoder = new PostgresTextEncoder(false); + test('Encode Bool', () { + final encoder = PostgresTextEncoder(false); - expect(encoder.convert(true), "TRUE"); - expect(encoder.convert(false), "FALSE"); + expect(encoder.convert(true), 'TRUE'); + expect(encoder.convert(false), 'FALSE'); }); - test("Encode JSONB", () { - final encoder = new PostgresTextEncoder(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}"); + 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 = new PostgresTextEncoder(false); + test('Attempt to infer unknown type throws exception', () { + final encoder = PostgresTextEncoder(false); try { encoder.convert([]); fail('unreachable'); } on PostgreSQLException catch (e) { - expect(e.toString(), contains("Could not infer type")); + expect(e.toString(), contains('Could not infer type')); } }); }); - test("UTF8String caches string regardless of which method is called first", + test('UTF8String caches string regardless of which method is called first', () { - var u = new UTF8BackedString("abcd"); - var v = new UTF8BackedString("abcd"); + final u = UTF8BackedString('abcd'); + final v = UTF8BackedString('abcd'); u.utf8Length; v.utf8Bytes; @@ -366,34 +363,34 @@ void main() { expect(v.hasCachedBytes, true); }); - test("Invalid UUID encoding", () { - final converter = new PostgresBinaryEncoder(PostgreSQLDataType.uuid); + test('Invalid UUID encoding', () { + final converter = PostgresBinaryEncoder(PostgreSQLDataType.uuid); try { - converter.convert("z0000000-0000-0000-0000-000000000000"); + converter.convert('z0000000-0000-0000-0000-000000000000'); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Invalid UUID string")); + expect(e.toString(), contains('Invalid UUID string')); } try { converter.convert(123123); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Invalid type for parameter")); + expect(e.toString(), contains('Invalid type for parameter')); } try { - converter.convert("0000000-0000-0000-0000-000000000000"); + converter.convert('0000000-0000-0000-0000-000000000000'); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Invalid UUID string")); + expect(e.toString(), contains('Invalid UUID string')); } try { - converter.convert("00000000-0000-0000-0000-000000000000f"); + converter.convert('00000000-0000-0000-0000-000000000000f'); fail('unreachable'); } on FormatException catch (e) { - expect(e.toString(), contains("Invalid UUID string")); + expect(e.toString(), contains('Invalid UUID string')); } }); } @@ -401,13 +398,13 @@ void main() { Future expectInverse(dynamic value, PostgreSQLDataType dataType) async { final type = PostgreSQLFormat.dataTypeStringForDataType(dataType); - await conn.execute("CREATE TEMPORARY TABLE IF NOT EXISTS t (v $type)"); + await conn.execute('CREATE TEMPORARY TABLE IF NOT EXISTS t (v $type)'); final result = await conn.query( - "INSERT INTO t (v) VALUES (${PostgreSQLFormat.id("v", type: dataType)}) RETURNING v", - substitutionValues: {"v": value}); + 'INSERT INTO t (v) VALUES (${PostgreSQLFormat.id('v', type: dataType)}) RETURNING v', + substitutionValues: {'v': value}); expect(result.first.first, equals(value)); - final encoder = new PostgresBinaryEncoder(dataType); + final encoder = PostgresBinaryEncoder(dataType); final encodedValue = encoder.convert(value); if (dataType == PostgreSQLDataType.serial) { @@ -415,14 +412,14 @@ Future expectInverse(dynamic value, PostgreSQLDataType dataType) async { } else if (dataType == PostgreSQLDataType.bigSerial) { dataType = PostgreSQLDataType.bigInteger; } - var code; + int code; PostgresBinaryDecoder.typeMap.forEach((key, type) { if (type == dataType) { code = key; } }); - final decoder = new PostgresBinaryDecoder(code); + final decoder = PostgresBinaryDecoder(code); final decodedValue = decoder.convert(encodedValue); expect(decodedValue, value); diff --git a/test/framer_test.dart b/test/framer_test.dart index 13cf361..25f62d7 100644 --- a/test/framer_test.dart +++ b/test/framer_test.dart @@ -1,69 +1,72 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:buffer/buffer.dart'; +import 'package:test/test.dart'; + import 'package:postgres/src/message_window.dart'; import 'package:postgres/src/server_messages.dart'; -import 'package:test/test.dart'; -import 'dart:typed_data'; -import 'dart:io'; void main() { MessageFramer framer; setUp(() { - framer = new MessageFramer(); + framer = MessageFramer(); }); tearDown(() { flush(framer); }); - test("Perfectly sized message in one buffer", () { + test('Perfectly sized message in one buffer', () { framer.addBytes(bufferWithMessages([ messageWithBytes([1, 2, 3], 1) ])); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]) + ..bytes = Uint8List.fromList([1, 2, 3]) ]); }); - test("Two perfectly sized messages in one buffer", () { + test('Two perfectly sized messages in one buffer', () { framer.addBytes(bufferWithMessages([ messageWithBytes([1, 2, 3], 1), messageWithBytes([1, 2, 3, 4], 2) ])); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]), - new UnknownMessage() + ..bytes = Uint8List.fromList([1, 2, 3]), + UnknownMessage() ..code = 2 - ..bytes = new Uint8List.fromList([1, 2, 3, 4]) + ..bytes = Uint8List.fromList([1, 2, 3, 4]) ]); }); - test("Header fragment", () { - var message = messageWithBytes([1, 2, 3], 1); - var fragments = fragmentedMessageBuffer(message, 2); + test('Header fragment', () { + final message = messageWithBytes([1, 2, 3], 1); + final fragments = fragmentedMessageBuffer(message, 2); framer.addBytes(fragments.first); expect(framer.messageQueue, isEmpty); framer.addBytes(fragments.last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]) + ..bytes = Uint8List.fromList([1, 2, 3]) ]); }); - test("Two header fragments", () { - var message = messageWithBytes([1, 2, 3], 1); - var fragments = fragmentedMessageBuffer(message, 2); - var moreFragments = fragmentedMessageBuffer(fragments.first, 1); + test('Two header fragments', () { + final message = messageWithBytes([1, 2, 3], 1); + final fragments = fragmentedMessageBuffer(message, 2); + final moreFragments = fragmentedMessageBuffer(fragments.first, 1); framer.addBytes(moreFragments.first); expect(framer.messageQueue, isEmpty); @@ -73,18 +76,18 @@ void main() { framer.addBytes(fragments.last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]) + ..bytes = Uint8List.fromList([1, 2, 3]) ]); }); - test("One message + header fragment", () { - var message1 = messageWithBytes([1, 2, 3], 1); - var message2 = messageWithBytes([2, 2, 3], 2); - var message2Fragments = fragmentedMessageBuffer(message2, 3); + test('One message + header fragment', () { + final message1 = messageWithBytes([1, 2, 3], 1); + final message2 = messageWithBytes([2, 2, 3], 2); + final message2Fragments = fragmentedMessageBuffer(message2, 3); framer.addBytes(bufferWithMessages([message1, message2Fragments.first])); @@ -92,21 +95,21 @@ void main() { framer.addBytes(message2Fragments.last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]), - new UnknownMessage() + ..bytes = Uint8List.fromList([1, 2, 3]), + UnknownMessage() ..code = 2 - ..bytes = new Uint8List.fromList([2, 2, 3]), + ..bytes = Uint8List.fromList([2, 2, 3]), ]); }); - test("Message + header, missing rest of buffer", () { - var message1 = messageWithBytes([1, 2, 3], 1); - var message2 = messageWithBytes([2, 2, 3], 2); - var message2Fragments = fragmentedMessageBuffer(message2, 5); + test('Message + header, missing rest of buffer', () { + final message1 = messageWithBytes([1, 2, 3], 1); + final message2 = messageWithBytes([2, 2, 3], 2); + final message2Fragments = fragmentedMessageBuffer(message2, 5); framer.addBytes(bufferWithMessages([message1, message2Fragments.first])); @@ -114,38 +117,38 @@ void main() { framer.addBytes(message2Fragments.last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]), - new UnknownMessage() + ..bytes = Uint8List.fromList([1, 2, 3]), + UnknownMessage() ..code = 2 - ..bytes = new Uint8List.fromList([2, 2, 3]), + ..bytes = Uint8List.fromList([2, 2, 3]), ]); }); - test("Message body spans two packets", () { - var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); - var fragments = fragmentedMessageBuffer(message, 8); + test('Message body spans two packets', () { + final message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); + final fragments = fragmentedMessageBuffer(message, 8); framer.addBytes(fragments.first); expect(framer.messageQueue, isEmpty); framer.addBytes(fragments.last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) + ..bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) ]); }); test( - "Message spans two packets, started in a packet that contained another message", + 'Message spans two packets, started in a packet that contained another message', () { - var earlierMessage = messageWithBytes([1, 2], 0); - var message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); + final earlierMessage = messageWithBytes([1, 2], 0); + final message = messageWithBytes([1, 2, 3, 4, 5, 6, 7], 1); framer.addBytes(bufferWithMessages( [earlierMessage, fragmentedMessageBuffer(message, 8).first])); @@ -153,20 +156,20 @@ void main() { framer.addBytes(fragmentedMessageBuffer(message, 8).last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 0 - ..bytes = new Uint8List.fromList([1, 2]), - new UnknownMessage() + ..bytes = Uint8List.fromList([1, 2]), + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) + ..bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) ]); }); - test("Message spans three packets, only part of header in the first", () { - var earlierMessage = messageWithBytes([1, 2], 0); - var message = + test('Message spans three packets, only part of header in the first', () { + final earlierMessage = messageWithBytes([1, 2], 0); + final message = messageWithBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 1); framer.addBytes(bufferWithMessages( @@ -182,44 +185,44 @@ void main() { fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6) .last); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 0 - ..bytes = new Uint8List.fromList([1, 2]), - new UnknownMessage() + ..bytes = Uint8List.fromList([1, 2]), + UnknownMessage() ..code = 1 ..bytes = - new Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) ]); }); - test("Frame with no data", () { + test('Frame with no data', () { framer.addBytes(bufferWithMessages([messageWithBytes([], 10)])); - var messages = framer.messageQueue.map((f) => f.message).toList(); - expect(messages, [new UnknownMessage()..code = 10]); + final messages = framer.messageQueue.map((f) => f.message).toList(); + expect(messages, [UnknownMessage()..code = 10]); }); } List messageWithBytes(List bytes, int messageID) { - var buffer = new BytesBuilder(); + final buffer = BytesBuilder(); buffer.addByte(messageID); - var lengthBuffer = new ByteData(4); + final lengthBuffer = ByteData(4); lengthBuffer.setUint32(0, bytes.length + 4); buffer.add(lengthBuffer.buffer.asUint8List()); buffer.add(bytes); return buffer.toBytes(); } -List> fragmentedMessageBuffer(List message, int pivotPoint) { - var l1 = message.sublist(0, pivotPoint); - var l2 = message.sublist(pivotPoint, message.length); - return [l1, l2]; +List fragmentedMessageBuffer(List message, int pivotPoint) { + final l1 = message.sublist(0, pivotPoint); + final l2 = message.sublist(pivotPoint, message.length); + return [castBytes(l1), castBytes(l2)]; } -List bufferWithMessages(List> messages) { - return new Uint8List.fromList(messages.expand((l) => l).toList()); +Uint8List bufferWithMessages(List> messages) { + return Uint8List.fromList(messages.expand((l) => l).toList()); } flush(MessageFramer framer) { @@ -228,10 +231,10 @@ flush(MessageFramer framer) { messageWithBytes([1, 2, 3], 1) ])); - var messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.map((f) => f.message).toList(); expect(messages, [ - new UnknownMessage() + UnknownMessage() ..code = 1 - ..bytes = new Uint8List.fromList([1, 2, 3]) + ..bytes = Uint8List.fromList([1, 2, 3]) ]); } diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 810a528..5f57756 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -5,7 +5,7 @@ import 'package:postgres/src/query.dart'; import 'package:test/test.dart'; void main() { - test("Ensure all types/format type mappings are available and accurate", () { + test('Ensure all types/format type mappings are available and accurate', () { PostgreSQLDataType.values .where((t) => t != PostgreSQLDataType.bigSerial && t != PostgreSQLDataType.serial) @@ -17,91 +17,92 @@ void main() { }); }); - test("Ensure bigserial gets translated to int8", () { + test('Ensure bigserial gets translated to int8', () { expect( PostgreSQLFormat.dataTypeStringForDataType(PostgreSQLDataType.serial), - "int4"); + 'int4'); }); - test("Ensure serial gets translated to int4", () { + test('Ensure serial gets translated to int4', () { expect( PostgreSQLFormat.dataTypeStringForDataType( PostgreSQLDataType.bigSerial), - "int8"); + 'int8'); }); - test("Simple replacement", () { - var result = PostgreSQLFormat.substitute("@id", {"id": 20}); - expect(result, equals("20")); + test('Simple replacement', () { + final result = PostgreSQLFormat.substitute('@id', {'id': 20}); + expect(result, equals('20')); }); - test("Trailing/leading space", () { - var result = PostgreSQLFormat.substitute(" @id ", {"id": 20}); - expect(result, equals(" 20 ")); + test('Trailing/leading space', () { + final result = PostgreSQLFormat.substitute(' @id ', {'id': 20}); + expect(result, equals(' 20 ')); }); - test("Two identifiers next to eachother", () { - var result = PostgreSQLFormat.substitute("@id@bob", {"id": 20, "bob": 13}); - expect(result, equals("2013")); + test('Two identifiers next to eachother', () { + final result = + PostgreSQLFormat.substitute('@id@bob', {'id': 20, 'bob': 13}); + expect(result, equals('2013')); }); - test("Identifier with underscores", () { - var result = PostgreSQLFormat.substitute("@_one_two", {"_one_two": 12}); - expect(result, equals("12")); + test('Identifier with underscores', () { + final result = PostgreSQLFormat.substitute('@_one_two', {'_one_two': 12}); + expect(result, equals('12')); }); - test("Identifier with type info", () { - var result = PostgreSQLFormat.substitute("@id:int2", {"id": 12}); - expect(result, equals("12")); + test('Identifier with type info', () { + final result = PostgreSQLFormat.substitute('@id:int2', {'id': 12}); + expect(result, equals('12')); }); - test("Identifiers next to eachother with type info", () { - var result = PostgreSQLFormat.substitute( - "@id:int2@foo:float4", {"id": 12, "foo": 2.0}); - expect(result, equals("122.0")); + test('Identifiers next to eachother with type info', () { + final result = PostgreSQLFormat.substitute( + '@id:int2@foo:float4', {'id': 12, 'foo': 2.0}); + expect(result, equals('122.0')); }); - test("Disambiguate PostgreSQL typecast", () { - var result = PostgreSQLFormat.substitute("@id::jsonb", {"id": "12"}); + test('Disambiguate PostgreSQL typecast', () { + final result = PostgreSQLFormat.substitute('@id::jsonb', {'id': '12'}); expect(result, "'12'::jsonb"); }); - test("PostgreSQL typecast appears in query", () { - var results = PostgreSQLFormat.substitute( + test('PostgreSQL typecast appears in query', () { + final results = PostgreSQLFormat.substitute( "SELECT * FROM t WHERE id=@id:int2 WHERE blob=@blob::jsonb AND blob='{\"a\":1}'::jsonb", - {"id": 2, "blob": "{\"key\":\"value\"}"}); + {'id': 2, 'blob': '{"key":"value"}'}); expect(results, "SELECT * FROM t WHERE id=2 WHERE blob='{\"key\":\"value\"}'::jsonb AND blob='{\"a\":1}'::jsonb"); }); - test("Can both provide type and typecast", () { - var results = PostgreSQLFormat.substitute( - "SELECT * FROM t WHERE id=@id:int2::int4", - {"id": 2, "blob": "{\"key\":\"value\"}"}); + test('Can both provide type and typecast', () { + final results = PostgreSQLFormat.substitute( + 'SELECT * FROM t WHERE id=@id:int2::int4', + {'id': 2, 'blob': '{"key":"value"}'}); - expect(results, "SELECT * FROM t WHERE id=2::int4"); + expect(results, 'SELECT * FROM t WHERE id=2::int4'); }); - test("UTF16 symbols with quotes", () { - var value = "'©™®'"; - var results = PostgreSQLFormat.substitute( - "INSERT INTO t (t) VALUES (@t)", {"t": value}); + test('UTF16 symbols with quotes', () { + final value = "'©™®'"; + final results = PostgreSQLFormat.substitute( + 'INSERT INTO t (t) VALUES (@t)', {'t': value}); expect(results, "INSERT INTO t (t) VALUES ('''©™®''')"); }); - test("UTF16 symbols with backslash", () { - var value = "'©\\™®'"; - var results = PostgreSQLFormat.substitute( - "INSERT INTO t (t) VALUES (@t)", {"t": value}); + test('UTF16 symbols with backslash', () { + final value = "'©\\™®'"; + final results = PostgreSQLFormat.substitute( + 'INSERT INTO t (t) VALUES (@t)', {'t': value}); expect(results, "INSERT INTO t (t) VALUES ( E'''©\\\\™®''')"); }); - test("String identifiers get escaped", () { - var result = PostgreSQLFormat.substitute( - "@id:text @foo", {"id": "1';select", "foo": "3\\4"}); + test('String identifiers get escaped', () { + final result = PostgreSQLFormat.substitute( + '@id:text @foo', {'id': "1';select", 'foo': '3\\4'}); // ' 1 ' ' ; s e l e c t ' sp sp E ' 3 \ \ 4 ' expect(utf8.encode(result), [ @@ -129,7 +130,7 @@ void main() { ]); }); - test("JSONB operator does not throw", () { + test('JSONB operator does not throw', () { final query = "SELECT id FROM table WHERE data @> '{\"key\": \"value\"}'"; final results = PostgreSQLFormat.substitute(query, {}); diff --git a/test/json_test.dart b/test/json_test.dart index 8a95e1d..cee5b27 100644 --- a/test/json_test.dart +++ b/test/json_test.dart @@ -5,145 +5,145 @@ void main() { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); - await connection.execute(""" + await connection.execute(''' CREATE TEMPORARY TABLE t (j jsonb) - """); + '''); }); tearDown(() async { await connection?.close(); }); - group("Storage", () { - test("Can store JSON String", () async { + group('Storage', () { + test('Can store JSON String', () async { var result = await connection .query("INSERT INTO t (j) VALUES ('\"xyz\"'::jsonb) RETURNING j"); expect(result, [ - ["xyz"] + ['xyz'] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ - ["xyz"] + ['xyz'] ]); }); - test("Can store JSON String with driver type annotation", () async { + test('Can store JSON String with driver type annotation', () async { var result = await connection.query( - "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", - substitutionValues: {"a": "xyz"}); + 'INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j', + substitutionValues: {'a': 'xyz'}); expect(result, [ - ["xyz"] + ['xyz'] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ - ["xyz"] + ['xyz'] ]); }); - test("Can store JSON Number", () async { + test('Can store JSON Number', () async { var result = await connection .query("INSERT INTO t (j) VALUES ('4'::jsonb) RETURNING j"); expect(result, [ [4] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ [4] ]); }); - test("Can store JSON Number with driver type annotation", () async { + test('Can store JSON Number with driver type annotation', () async { var result = await connection.query( - "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", - substitutionValues: {"a": 4}); + 'INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j', + substitutionValues: {'a': 4}); expect(result, [ [4] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ [4] ]); }); - test("Can store JSON map", () async { + test('Can store JSON map', () async { var result = await connection .query("INSERT INTO t (j) VALUES ('{\"a\":4}') RETURNING j"); expect(result, [ [ - {"a": 4} + {'a': 4} ] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ [ - {"a": 4} + {'a': 4} ] ]); }); - test("Can store JSON map with driver type annotation", () async { + test('Can store JSON map with driver type annotation', () async { var result = await connection.query( - "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", + 'INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j', substitutionValues: { - "a": {"a": 4} + 'a': {'a': 4} }); expect(result, [ [ - {"a": 4} + {'a': 4} ] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ [ - {"a": 4} + {'a': 4} ] ]); }); - test("Can store JSON list", () async { + test('Can store JSON list', () async { var result = await connection .query("INSERT INTO t (j) VALUES ('[{\"a\":4}]') RETURNING j"); expect(result, [ [ [ - {"a": 4} + {'a': 4} ] ] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ [ [ - {"a": 4} + {'a': 4} ] ] ]); }); - test("Can store JSON list with driver type annotation", () async { + test('Can store JSON list with driver type annotation', () async { var result = await connection.query( - "INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j", + 'INSERT INTO t (j) VALUES (@a:jsonb) RETURNING j', substitutionValues: { - "a": [ - {"a": 4} + 'a': [ + {'a': 4} ] }); expect(result, [ [ [ - {"a": 4} + {'a': 4} ] ] ]); - result = await connection.query("SELECT j FROM t"); + result = await connection.query('SELECT j FROM t'); expect(result, [ [ [ - {"a": 4} + {'a': 4} ] ] ]); diff --git a/test/map_return_test.dart b/test/map_return_test.dart index 2a1de3b..bdcb463 100644 --- a/test/map_return_test.dart +++ b/test/map_return_test.dart @@ -6,17 +6,17 @@ void main() { InterceptingConnection connection; setUp(() async { - connection = new InterceptingConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = InterceptingConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); - await connection.execute(""" + await connection.execute(''' CREATE TEMPORARY TABLE t (id int primary key, name text) - """); + '''); - await connection.execute(""" + await connection.execute(''' CREATE TEMPORARY TABLE u (id int primary key, name text, t_id int references t (id)) - """); + '''); await connection.execute("INSERT INTO t (id, name) VALUES (1, 'a')"); await connection.execute("INSERT INTO t (id, name) VALUES (2, 'b')"); @@ -33,106 +33,106 @@ void main() { await connection?.close(); }); - test("Get row map without specifying columns", () async { + test('Get row map without specifying columns', () async { final results = - await connection.mappedResultsQuery("SELECT * from t ORDER BY id ASC"); + await connection.mappedResultsQuery('SELECT * from t ORDER BY id ASC'); expect(results, [ { - "t": {"id": 1, "name": "a"} + 't': {'id': 1, 'name': 'a'} }, { - "t": {"id": 2, "name": "b"} + 't': {'id': 2, 'name': 'b'} }, { - "t": {"id": 3, "name": "c"} + 't': {'id': 3, 'name': 'c'} }, ]); }); - test("Get row map by with specified columns", () async { + test('Get row map by with specified columns', () async { final results = await connection - .mappedResultsQuery("SELECT name, id from t ORDER BY id ASC"); + .mappedResultsQuery('SELECT name, id from t ORDER BY id ASC'); expect(results, [ { - "t": {"id": 1, "name": "a"} + 't': {'id': 1, 'name': 'a'} }, { - "t": {"id": 2, "name": "b"} + 't': {'id': 2, 'name': 'b'} }, { - "t": {"id": 3, "name": "c"} + 't': {'id': 3, 'name': 'c'} }, ]); final nextResults = await connection - .mappedResultsQuery("SELECT name from t ORDER BY name DESC"); + .mappedResultsQuery('SELECT name from t ORDER BY name DESC'); expect(nextResults, [ { - "t": {"name": "c"} + 't': {'name': 'c'} }, { - "t": {"name": "b"} + 't': {'name': 'b'} }, { - "t": {"name": "a"} + 't': {'name': 'a'} }, ]); }); - test("Get row with joined row", () async { + test('Get row with joined row', () async { final results = await connection.mappedResultsQuery( - "SELECT t.name, t.id, u.id, u.name, u.t_id from t LEFT OUTER JOIN u ON t.id=u.t_id ORDER BY t.id ASC"); + 'SELECT t.name, t.id, u.id, u.name, u.t_id from t LEFT OUTER JOIN u ON t.id=u.t_id ORDER BY t.id ASC'); expect(results, [ { - "t": {"name": "a", "id": 1}, - "u": {"id": 1, "name": "ua", "t_id": 1} + 't': {'name': 'a', 'id': 1}, + 'u': {'id': 1, 'name': 'ua', 't_id': 1} }, { - "t": {"name": "a", "id": 1}, - "u": {"id": 2, "name": "ub", "t_id": 1} + 't': {'name': 'a', 'id': 1}, + 'u': {'id': 2, 'name': 'ub', 't_id': 1} }, { - "t": {"name": "b", "id": 2}, - "u": {"id": 3, "name": "uc", "t_id": 2} + 't': {'name': 'b', 'id': 2}, + 'u': {'id': 3, 'name': 'uc', 't_id': 2} }, { - "t": {"name": "c", "id": 3}, - "u": {"name": null, "id": null, "t_id": null} + 't': {'name': 'c', 'id': 3}, + 'u': {'name': null, 'id': null, 't_id': null} } ]); }); - test("Table names get cached", () async { - final regex = new RegExp( + test('Table names get cached', () async { + final regex = RegExp( "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN \\(([0-9]*)\\) ORDER BY oid ASC"); final oids = []; - await connection.mappedResultsQuery("SELECT id FROM t"); + await connection.mappedResultsQuery('SELECT id FROM t'); expect(connection.queries.length, 1); var match = regex.firstMatch(connection.queries.first); oids.add(match.group(1)); connection.queries.clear(); - await connection.mappedResultsQuery("SELECT id FROM t"); + await connection.mappedResultsQuery('SELECT id FROM t'); expect(connection.queries.length, 0); await connection.mappedResultsQuery( - "SELECT t.id, u.id FROM t LEFT OUTER JOIN u ON t.id=u.t_id"); + 'SELECT t.id, u.id FROM t LEFT OUTER JOIN u ON t.id=u.t_id'); expect(connection.queries.length, 1); match = regex.firstMatch(connection.queries.first); expect(oids.contains(match.group(1)), false); oids.add(match.group(1)); connection.queries.clear(); - await connection.mappedResultsQuery("SELECT u.id FROM u"); + await connection.mappedResultsQuery('SELECT u.id FROM u'); expect(connection.queries.length, 0); }); - test("Non-table mappedResultsQuery succeeds", () async { - final result = await connection.mappedResultsQuery("SELECT 1"); + test('Non-table mappedResultsQuery succeeds', () async { + final result = await connection.mappedResultsQuery('SELECT 1'); expect(result, [ { - null: {"?column?": 1} + null: {'?column?': 1} } ]); }); @@ -140,15 +140,15 @@ void main() { class InterceptingConnection extends PostgreSQLConnection { InterceptingConnection(String host, int port, String databaseName, - {String username: null, String password: null}) + {String username, String password}) : super(host, port, databaseName, username: username, password: password); List queries = []; @override Future>> query(String fmtString, - {Map substitutionValues: null, - bool allowReuse: true, + {Map substitutionValues, + bool allowReuse = true, int timeoutInSeconds}) { queries.add(fmtString); return super.query(fmtString, diff --git a/test/notification_test.dart b/test/notification_test.dart index fbe32e7..576450f 100644 --- a/test/notification_test.dart +++ b/test/notification_test.dart @@ -4,13 +4,13 @@ import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; void main() { - group("Successful notifications", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + group('Successful notifications', () { + var connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); }); @@ -18,91 +18,91 @@ void main() { await connection.close(); }); - test("Notification Response", () async { - var channel = 'virtual'; - var payload = 'This is the payload'; - var futureMsg = connection.notifications.first; - await connection.execute("LISTEN $channel;" + test('Notification Response', () async { + final channel = 'virtual'; + final payload = 'This is the payload'; + final futureMsg = connection.notifications.first; + await connection.execute('LISTEN $channel;' "NOTIFY $channel, '$payload';"); - var msg = await futureMsg.timeout(new Duration(milliseconds: 200)); + final msg = await futureMsg.timeout(Duration(milliseconds: 200)); expect(msg.channel, channel); expect(msg.payload, payload); }); - test("Notification Response empty payload", () async { - var channel = 'virtual'; - var futureMsg = connection.notifications.first; - await connection.execute("LISTEN $channel;" - "NOTIFY $channel;"); + test('Notification Response empty payload', () async { + final channel = 'virtual'; + final futureMsg = connection.notifications.first; + await connection.execute('LISTEN $channel;' + 'NOTIFY $channel;'); - var msg = await futureMsg.timeout(new Duration(milliseconds: 200)); + final msg = await futureMsg.timeout(Duration(milliseconds: 200)); expect(msg.channel, channel); expect(msg.payload, ''); }); - test("Notification UNLISTEN", () async { - var channel = 'virtual'; - var payload = 'This is the payload'; + test('Notification UNLISTEN', () async { + final channel = 'virtual'; + final payload = 'This is the payload'; var futureMsg = connection.notifications.first; - await connection.execute("LISTEN $channel;" + await connection.execute('LISTEN $channel;' "NOTIFY $channel, '$payload';"); - var msg = await futureMsg.timeout(new Duration(milliseconds: 200)); + final msg = await futureMsg.timeout(Duration(milliseconds: 200)); expect(msg.channel, channel); expect(msg.payload, payload); - await connection.execute("UNLISTEN $channel;"); + await connection.execute('UNLISTEN $channel;'); futureMsg = connection.notifications.first; try { await connection.execute("NOTIFY $channel, '$payload';"); - await futureMsg.timeout(new Duration(milliseconds: 200)); + await futureMsg.timeout(Duration(milliseconds: 200)); fail('There should be no notification'); } on TimeoutException catch (_) {} }); - test("Notification many channel", () async { - Map countResponse = new Map(); + test('Notification many channel', () async { + final countResponse = {}; int totalCountResponse = 0; - Completer finishExecute = new Completer(); + final finishExecute = Completer(); connection.notifications.listen((msg) { - int count = countResponse[msg.channel]; + final count = countResponse[msg.channel]; countResponse[msg.channel] = (count ?? 0) + 1; totalCountResponse++; if (totalCountResponse == 20) finishExecute.complete(); }); - var channel1 = 'virtual1'; - var channel2 = 'virtual2'; + final channel1 = 'virtual1'; + final channel2 = 'virtual2'; - var notifier = () async { + final notifier = () async { for (int i = 0; i < 5; i++) { - await connection.execute("NOTIFY $channel1;" - "NOTIFY $channel2;"); + await connection.execute('NOTIFY $channel1;' + 'NOTIFY $channel2;'); } }; - await connection.execute("LISTEN $channel1;"); + await connection.execute('LISTEN $channel1;'); await notifier(); - await connection.execute("LISTEN $channel2;"); + await connection.execute('LISTEN $channel2;'); await notifier(); - await connection.execute("UNLISTEN $channel1;"); + await connection.execute('UNLISTEN $channel1;'); await notifier(); - await connection.execute("UNLISTEN $channel2;"); + await connection.execute('UNLISTEN $channel2;'); await notifier(); - await finishExecute.future.timeout(new Duration(milliseconds: 200)); + await finishExecute.future.timeout(Duration(milliseconds: 200)); expect(countResponse[channel1], 10); expect(countResponse[channel2], 10); - }, timeout: new Timeout(new Duration(seconds: 5))); + }, timeout: Timeout(Duration(seconds: 5))); }); } diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index 77127ae..a6984eb 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -1,406 +1,408 @@ -import 'package:postgres/postgres.dart'; -import 'package:test/test.dart'; import 'dart:async'; import 'dart:mirrors'; +import 'package:test/test.dart'; + +import 'package:postgres/postgres.dart'; + String sid(String id, PostgreSQLDataType dt) => PostgreSQLFormat.id(id, type: dt); void main() { - group("Retaining type information", () { + group('Retaining type information', () { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); await connection.execute( - "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + 'CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)'); }); tearDown(() async { await connection.close(); }); - test("Call query multiple times with all parameter types succeeds", + test('Call query multiple times with all parameter types succeeds', () async { - var insertQueryString = - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " - "(${sid("i", PostgreSQLDataType.integer)}, ${sid("bi", PostgreSQLDataType.bigInteger)}," - "${sid("bl", PostgreSQLDataType.boolean)}, ${sid("si", PostgreSQLDataType.smallInteger)}," - "${sid("t", PostgreSQLDataType.text)}, ${sid("f", PostgreSQLDataType.real)}," - "${sid("d", PostgreSQLDataType.double)}, ${sid("dt", PostgreSQLDataType.date)}," - "${sid("ts", PostgreSQLDataType.timestampWithoutTimezone)}, ${sid("tsz", PostgreSQLDataType.timestampWithTimezone)}" - ") returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz"; + final insertQueryString = + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES ' + '(${sid('i', PostgreSQLDataType.integer)}, ${sid('bi', PostgreSQLDataType.bigInteger)},' + '${sid('bl', PostgreSQLDataType.boolean)}, ${sid('si', PostgreSQLDataType.smallInteger)},' + '${sid('t', PostgreSQLDataType.text)}, ${sid('f', PostgreSQLDataType.real)},' + '${sid('d', PostgreSQLDataType.double)}, ${sid('dt', PostgreSQLDataType.date)},' + '${sid('ts', PostgreSQLDataType.timestampWithoutTimezone)}, ${sid('tsz', PostgreSQLDataType.timestampWithTimezone)}' + ') returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz'; var results = await connection.query(insertQueryString, substitutionValues: { - "i": 1, - "bi": 2, - "bl": true, - "si": 3, - "t": "foobar", - "f": 5.0, - "d": 6.0, - "dt": new DateTime.utc(2000), - "ts": new DateTime.utc(2000, 2), - "tsz": new DateTime.utc(2000, 3) + 'i': 1, + 'bi': 2, + 'bl': true, + 'si': 3, + 't': 'foobar', + 'f': 5.0, + 'd': 6.0, + 'dt': DateTime.utc(2000), + 'ts': DateTime.utc(2000, 2), + 'tsz': DateTime.utc(2000, 3) }); expect(hasCachedQueryNamed(connection, insertQueryString), true); - var expectedRow1 = [ + final expectedRow1 = [ 1, 1, 2, 1, true, 3, - "foobar", + 'foobar', 5.0, 6.0, - new DateTime.utc(2000), - new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3) + DateTime.utc(2000), + DateTime.utc(2000, 2), + DateTime.utc(2000, 3) ]; expect(results, [expectedRow1]); results = await connection.query(insertQueryString, substitutionValues: { - "i": 2, - "bi": 3, - "bl": false, - "si": 4, - "t": "barfoo", - "f": 6.0, - "d": 7.0, - "dt": new DateTime.utc(2001), - "ts": new DateTime.utc(2001, 2), - "tsz": new DateTime.utc(2001, 3) + 'i': 2, + 'bi': 3, + 'bl': false, + 'si': 4, + 't': 'barfoo', + 'f': 6.0, + 'd': 7.0, + 'dt': DateTime.utc(2001), + 'ts': DateTime.utc(2001, 2), + 'tsz': DateTime.utc(2001, 3) }); expect(hasCachedQueryNamed(connection, insertQueryString), true); - var expectedRow2 = [ + final expectedRow2 = [ 2, 2, 3, 2, false, 4, - "barfoo", + 'barfoo', 6.0, 7.0, - new DateTime.utc(2001), - new DateTime.utc(2001, 2), - new DateTime.utc(2001, 3) + DateTime.utc(2001), + DateTime.utc(2001, 2), + DateTime.utc(2001, 3) ]; expect(results, [expectedRow2]); results = await connection - .query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + .query('select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t'); expect(results, [expectedRow1, expectedRow2]); expect( hasCachedQueryNamed(connection, - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"), + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t'), true); results = await connection.query( - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", - substitutionValues: {"i": 0}); + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i', + substitutionValues: {'i': 0}); expect(results, []); results = await connection.query( - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", - substitutionValues: {"i": 2}); + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i', + substitutionValues: {'i': 2}); expect(results, [expectedRow1]); results = await connection.query( - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", - substitutionValues: {"i": 5}); + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i', + substitutionValues: {'i': 5}); expect(results, [expectedRow1, expectedRow2]); expect(hasCachedQueryNamed(connection, insertQueryString), true); }); - test("Call query multiple times without type data succeeds ", () async { - var insertQueryString = - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " - "(@i, @bi, @bl, @si, @t, @f, @d, @dt, @ts, @tsz) " - "returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz"; + test('Call query multiple times without type data succeeds ', () async { + final insertQueryString = + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES ' + '(@i, @bi, @bl, @si, @t, @f, @d, @dt, @ts, @tsz) ' + 'returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz'; var results = await connection.query(insertQueryString, substitutionValues: { - "i": 1, - "bi": 2, - "bl": true, - "si": 3, - "t": "foobar", - "f": 5.0, - "d": 6.0, - "dt": new DateTime.utc(2000), - "ts": new DateTime.utc(2000, 2), - "tsz": new DateTime.utc(2000, 3) + 'i': 1, + 'bi': 2, + 'bl': true, + 'si': 3, + 't': 'foobar', + 'f': 5.0, + 'd': 6.0, + 'dt': DateTime.utc(2000), + 'ts': DateTime.utc(2000, 2), + 'tsz': DateTime.utc(2000, 3) }); - var expectedRow1 = [ + final expectedRow1 = [ 1, 1, 2, 1, true, 3, - "foobar", + 'foobar', 5.0, 6.0, - new DateTime.utc(2000), - new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3) + DateTime.utc(2000), + DateTime.utc(2000, 2), + DateTime.utc(2000, 3) ]; expect(results, [expectedRow1]); results = await connection.query(insertQueryString, substitutionValues: { - "i": 2, - "bi": 3, - "bl": false, - "si": 4, - "t": "barfoo", - "f": 6.0, - "d": 7.0, - "dt": new DateTime.utc(2001), - "ts": new DateTime.utc(2001, 2), - "tsz": new DateTime.utc(2001, 3) + 'i': 2, + 'bi': 3, + 'bl': false, + 'si': 4, + 't': 'barfoo', + 'f': 6.0, + 'd': 7.0, + 'dt': DateTime.utc(2001), + 'ts': DateTime.utc(2001, 2), + 'tsz': DateTime.utc(2001, 3) }); - var expectedRow2 = [ + final expectedRow2 = [ 2, 2, 3, 2, false, 4, - "barfoo", + 'barfoo', 6.0, 7.0, - new DateTime.utc(2001), - new DateTime.utc(2001, 2), - new DateTime.utc(2001, 3) + DateTime.utc(2001), + DateTime.utc(2001, 2), + DateTime.utc(2001, 3) ]; expect(results, [expectedRow2]); }); - test("Call query multiple times with partial parameter type info succeeds", + test('Call query multiple times with partial parameter type info succeeds', () async { - var insertQueryString = - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES " - "(${sid("i", PostgreSQLDataType.integer)}, @bi," - "${sid("bl", PostgreSQLDataType.boolean)}, @si," - "${sid("t", PostgreSQLDataType.text)}, @f," - "${sid("d", PostgreSQLDataType.double)}, @dt," - "${sid("ts", PostgreSQLDataType.timestampWithoutTimezone)}, @tsz" - ") returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz"; + final insertQueryString = + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) VALUES ' + '(${sid('i', PostgreSQLDataType.integer)}, @bi,' + '${sid('bl', PostgreSQLDataType.boolean)}, @si,' + '${sid('t', PostgreSQLDataType.text)}, @f,' + '${sid('d', PostgreSQLDataType.double)}, @dt,' + '${sid('ts', PostgreSQLDataType.timestampWithoutTimezone)}, @tsz' + ') returning i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz'; var results = await connection.query(insertQueryString, substitutionValues: { - "i": 1, - "bi": 2, - "bl": true, - "si": 3, - "t": "foobar", - "f": 5.0, - "d": 6.0, - "dt": new DateTime.utc(2000), - "ts": new DateTime.utc(2000, 2), - "tsz": new DateTime.utc(2000, 3) + 'i': 1, + 'bi': 2, + 'bl': true, + 'si': 3, + 't': 'foobar', + 'f': 5.0, + 'd': 6.0, + 'dt': DateTime.utc(2000), + 'ts': DateTime.utc(2000, 2), + 'tsz': DateTime.utc(2000, 3) }); - var expectedRow1 = [ + final expectedRow1 = [ 1, 1, 2, 1, true, 3, - "foobar", + 'foobar', 5.0, 6.0, - new DateTime.utc(2000), - new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3) + DateTime.utc(2000), + DateTime.utc(2000, 2), + DateTime.utc(2000, 3) ]; expect(results, [expectedRow1]); results = await connection.query(insertQueryString, substitutionValues: { - "i": 2, - "bi": 3, - "bl": false, - "si": 4, - "t": "barfoo", - "f": 6.0, - "d": 7.0, - "dt": new DateTime.utc(2001), - "ts": new DateTime.utc(2001, 2), - "tsz": new DateTime.utc(2001, 3) + 'i': 2, + 'bi': 3, + 'bl': false, + 'si': 4, + 't': 'barfoo', + 'f': 6.0, + 'd': 7.0, + 'dt': DateTime.utc(2001), + 'ts': DateTime.utc(2001, 2), + 'tsz': DateTime.utc(2001, 3) }); - var expectedRow2 = [ + final expectedRow2 = [ 2, 2, 3, 2, false, 4, - "barfoo", + 'barfoo', 6.0, 7.0, - new DateTime.utc(2001), - new DateTime.utc(2001, 2), - new DateTime.utc(2001, 3) + DateTime.utc(2001), + DateTime.utc(2001, 2), + DateTime.utc(2001, 3) ]; expect(results, [expectedRow2]); results = await connection - .query("select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + .query('select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t'); expect(results, [expectedRow1, expectedRow2]); results = await connection.query( - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", - substitutionValues: {"i": 0}); + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i', + substitutionValues: {'i': 0}); expect(results, []); results = await connection.query( - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", - substitutionValues: {"i": 2}); + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i', + substitutionValues: {'i': 2}); expect(results, [expectedRow1]); results = await connection.query( - "select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i", - substitutionValues: {"i": 5}); + 'select i, s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t where i < @i', + substitutionValues: {'i': 5}); expect(results, [expectedRow1, expectedRow2]); }); }); - group("Mixing prepared statements", () { + group('Mixing prepared statements', () { PostgreSQLConnection connection; setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); await connection.execute( - "CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)"); - await connection.execute("INSERT INTO t (i1, i2) VALUES (0, 1)"); - await connection.execute("INSERT INTO t (i1, i2) VALUES (1, 2)"); - await connection.execute("INSERT INTO t (i1, i2) VALUES (2, 3)"); - await connection.execute("INSERT INTO t (i1, i2) VALUES (3, 4)"); + 'CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)'); + await connection.execute('INSERT INTO t (i1, i2) VALUES (0, 1)'); + await connection.execute('INSERT INTO t (i1, i2) VALUES (1, 2)'); + await connection.execute('INSERT INTO t (i1, i2) VALUES (2, 3)'); + await connection.execute('INSERT INTO t (i1, i2) VALUES (3, 4)'); }); tearDown(() async { await connection.close(); }); - test("Call query multiple times, mixing in unnammed queries, succeeds", + test('Call query multiple times, mixing in unnammed queries, succeeds', () async { var results = await connection.query( - "select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 1}); + 'select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 1}); expect(results, [ [2, 3], [3, 4] ]); - results = await connection.query("select i1,i2 from t where i1 > @i1", - substitutionValues: {"i1": 1}, allowReuse: false); + results = await connection.query('select i1,i2 from t where i1 > @i1', + substitutionValues: {'i1': 1}, allowReuse: false); expect(results, [ [2, 3], [3, 4] ]); - results = await connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 2}); + results = await connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 2}); expect(results, [ [3, 4] ]); - results = await connection.query("select i1,i2 from t where i1 > @i1", - substitutionValues: {"i1": 0}, allowReuse: false); + results = await connection.query('select i1,i2 from t where i1 > @i1', + substitutionValues: {'i1': 0}, allowReuse: false); expect(results, [ [1, 2], [2, 3], [3, 4] ]); - results = await connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 2}); + results = await connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 2}); expect(results, [ [3, 4] ]); expect( hasCachedQueryNamed( - connection, "select i1, i2 from t where i1 > @i1"), + connection, 'select i1, i2 from t where i1 > @i1'), true); expect(cachedQueryMap(connection).length, 1); }); - test("Call query multiple times, mixing in other named queries, succeeds", + test('Call query multiple times, mixing in other named queries, succeeds', () async { var results = await connection.query( - "select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 1}); + 'select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 1}); expect(results, [ [2, 3], [3, 4] ]); - results = await connection.query("select i1,i2 from t where i2 < @i2", - substitutionValues: {"i2": 1}); + results = await connection.query('select i1,i2 from t where i2 < @i2', + substitutionValues: {'i2': 1}); expect(results, []); - results = await connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 2}); + results = await connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 2}); expect(results, [ [3, 4] ]); - results = await connection.query("select i1,i2 from t where i2 < @i2", - substitutionValues: {"i2": 2}); + results = await connection.query('select i1,i2 from t where i2 < @i2', + substitutionValues: {'i2': 2}); expect(results, [ [0, 1] ]); - results = await connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 2}); + results = await connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 2}); expect(results, [ [3, 4] ]); expect( hasCachedQueryNamed( - connection, "select i1, i2 from t where i1 > @i1"), + connection, 'select i1, i2 from t where i1 > @i1'), true); expect( - hasCachedQueryNamed(connection, "select i1,i2 from t where i2 < @i2"), + hasCachedQueryNamed(connection, 'select i1,i2 from t where i2 < @i2'), true); expect(cachedQueryMap(connection).length, 2); }); test( - "Call a bunch of named and unnamed queries without awaiting, still process correctly", + 'Call a bunch of named and unnamed queries without awaiting, still process correctly', () async { - var futures = [ - connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 1}), - connection.execute("select 1"), - connection.query("select i1,i2 from t where i2 < @i2", - substitutionValues: {"i2": 1}), - connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 2}), - connection.query("select 1", allowReuse: false), - connection.query("select i1,i2 from t where i2 < @i2", - substitutionValues: {"i2": 2}), - connection.query("select i1, i2 from t where i1 > @i1", - substitutionValues: {"i1": 2}) + final futures = [ + connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 1}), + connection.execute('select 1'), + connection.query('select i1,i2 from t where i2 < @i2', + substitutionValues: {'i2': 1}), + connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 2}), + connection.query('select 1', allowReuse: false), + connection.query('select i1,i2 from t where i2 < @i2', + substitutionValues: {'i2': 2}), + connection.query('select i1, i2 from t where i1 > @i1', + substitutionValues: {'i1': 2}) ]; - var results = await Future.wait(futures); + final results = await Future.wait(futures); expect(results, [ [ [2, 3], @@ -423,42 +425,42 @@ void main() { ]); }); - test("Make a prepared query that has no parameters", () async { - var results = await connection.query("select 1"); + test('Make a prepared query that has no parameters', () async { + var results = await connection.query('select 1'); expect(results, [ [1] ]); - results = await connection.query("select 1"); + results = await connection.query('select 1'); expect(results, [ [1] ]); }); }); - group("Failure cases", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + group('Failure cases', () { + var connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); await connection.execute( - "CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)"); + 'CREATE TEMPORARY TABLE t (i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint, t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz)'); await connection.execute( - "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); + 'CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);'); await connection - .execute("CREATE TEMPORARY TABLE n (i1 int, i2 int not null);"); + .execute('CREATE TEMPORARY TABLE n (i1 int, i2 int not null);'); }); tearDown(() async { await connection.close(); }); - test("A failed parse does not generate cached query", () async { + test('A failed parse does not generate cached query', () async { try { - await connection.query("ljkasd"); + await connection.query('ljkasd'); expect(true, false); } on PostgreSQLException {} @@ -466,13 +468,13 @@ void main() { }); test( - "Trying to parse/describe a query with inaccurate types fails and does not cache query", + 'Trying to parse/describe a query with inaccurate types fails and does not cache query', () async { - var string = - "insert into u (i1, i2) values (@i1:text, @i2:text) returning i1, i2"; + final string = + 'insert into u (i1, i2) values (@i1:text, @i2:text) returning i1, i2'; try { await connection - .query(string, substitutionValues: {"i1": "foo", "i2": "bar"}); + .query(string, substitutionValues: {'i1': 'foo', 'i2': 'bar'}); expect(true, false); } on PostgreSQLException {} @@ -481,23 +483,24 @@ void main() { }); test( - "A failed bind on initial query fails query, but can still make query later", + 'A failed bind on initial query fails query, but can still make query later', () async { - var string = "insert into u (i1, i2) values (@i1, @i2) returning i1, i2"; + final string = + 'insert into u (i1, i2) values (@i1, @i2) returning i1, i2'; try { await connection - .query(string, substitutionValues: {"i1": "foo", "i2": "bar"}); + .query(string, substitutionValues: {'i1': 'foo', 'i2': 'bar'}); expect(true, false); } on PostgreSQLException {} expect(hasCachedQueryNamed(connection, string), false); - var results = await connection.query("select i1, i2 from u"); + var results = await connection.query('select i1, i2 from u'); expect(results, []); - await connection.query(string, substitutionValues: {"i1": 1, "i2": 2}); - results = await connection.query("select i1, i2 from u"); + await connection.query(string, substitutionValues: {'i1': 1, 'i2': 2}); + results = await connection.query('select i1, i2 from u'); expect(results, [ [1, 2] ]); @@ -505,28 +508,28 @@ void main() { }); test( - "Cached query that works the first time, wrong type for params the next time throws early error but can still be used", + 'Cached query that works the first time, wrong type for params the next time throws early error but can still be used', () async { await connection.query( - "insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", - substitutionValues: {"i1": 1, "i2": 2}); + 'insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2', + substitutionValues: {'i1': 1, 'i2': 2}); await connection.query( - "insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", - substitutionValues: {"i1": 2, "i2": 3}); + 'insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2', + substitutionValues: {'i1': 2, 'i2': 3}); - var string = "select i1, i2 from u where i1 = @i:int4"; + final string = 'select i1, i2 from u where i1 = @i:int4'; var results = - await connection.query(string, substitutionValues: {"i": 1}); + await connection.query(string, substitutionValues: {'i': 1}); expect(results, [ [1, 2] ]); expect(hasCachedQueryNamed(connection, string), true); try { - await connection.query(string, substitutionValues: {"i": "foo"}); + await connection.query(string, substitutionValues: {'i': 'foo'}); } on FormatException {} - results = await connection.query(string, substitutionValues: {"i": 2}); + results = await connection.query(string, substitutionValues: {'i': 2}); expect(results, [ [2, 3] ]); @@ -534,20 +537,20 @@ void main() { }); test( - "Send two queries that will be the same prepared statement async, first one fails on bind", + 'Send two queries that will be the same prepared statement async, first one fails on bind', () async { await connection.query( - "insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", - substitutionValues: {"i1": 1, "i2": 2}, + 'insert into u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2', + substitutionValues: {'i1': 1, 'i2': 2}, allowReuse: false); - var string = "select i1, i2 from u where i1 = @i:int4"; + final string = 'select i1, i2 from u where i1 = @i:int4'; // ignore: unawaited_futures connection - .query(string, substitutionValues: {"i": "foo"}).catchError((e) {}); + .query(string, substitutionValues: {'i': 'foo'}).catchError((e) {}); - var results = - await connection.query(string, substitutionValues: {"i": 1}); + final results = + await connection.query(string, substitutionValues: {'i': 1}); expect(results, [ [1, 2] @@ -559,8 +562,8 @@ void main() { } Map cachedQueryMap(PostgreSQLConnection connection) { - var cacheMirror = reflect(connection).type.declarations.values.firstWhere( - (DeclarationMirror dm) => dm.simpleName.toString().contains("_cache")); + final cacheMirror = reflect(connection).type.declarations.values.firstWhere( + (DeclarationMirror dm) => dm.simpleName.toString().contains('_cache')); return reflect(connection) .getField(cacheMirror.simpleName) .getField(#queries) diff --git a/test/query_test.dart b/test/query_test.dart index cdc87f0..dcc5164 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -3,267 +3,267 @@ import 'package:test/test.dart'; import 'package:postgres/src/types.dart'; void main() { - group("Successful queries", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + group('Successful queries', () { + var connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); - await connection.execute("CREATE TEMPORARY TABLE t " - "(i int, s serial, bi bigint, " - "bs bigserial, bl boolean, si smallint, " - "t text, f real, d double precision, " - "dt date, ts timestamp, tsz timestamptz, j jsonb, u uuid)"); + await connection.execute('CREATE TEMPORARY TABLE t ' + '(i int, s serial, bi bigint, ' + 'bs bigserial, bl boolean, si smallint, ' + 't text, f real, d double precision, ' + 'dt date, ts timestamp, tsz timestamptz, j jsonb, u uuid)'); await connection.execute( - "CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);"); + 'CREATE TEMPORARY TABLE u (i1 int not null, i2 int not null);'); await connection - .execute("CREATE TEMPORARY TABLE n (i1 int, i2 int not null);"); + .execute('CREATE TEMPORARY TABLE n (i1 int, i2 int not null);'); }); tearDown(() async { await connection.close(); }); - test("UTF16 strings in value", () async { + test('UTF16 strings in value', () async { var result = await connection.query( - "INSERT INTO t (t) values " + 'INSERT INTO t (t) values ' "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})" - "returning t", + 'returning t', substitutionValues: { - "t": "°∆", + 't': '°∆', }); - var expectedRow = ["°∆"]; + final expectedRow = ['°∆']; expect(result, [expectedRow]); - result = await connection.query("select t from t"); + result = await connection.query('select t from t'); expect(result, [expectedRow]); }); - test("UTF16 strings in query", () async { + test('UTF16 strings in query', () async { var result = await connection.query("INSERT INTO t (t) values ('°∆') RETURNING t"); - var expectedRow = ["°∆"]; + final expectedRow = ['°∆']; expect(result, [expectedRow]); - result = await connection.query("select t from t"); + result = await connection.query('select t from t'); expect(result, [expectedRow]); }); - test("UTF16 strings in value with escape characters", () async { + test('UTF16 strings in value with escape characters', () async { await connection.execute( - "INSERT INTO t (t) values " - "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", + 'INSERT INTO t (t) values ' + '(${PostgreSQLFormat.id('t', type: PostgreSQLDataType.text)})', substitutionValues: { - "t": "'©™®'", + 't': "'©™®'", }); - var expectedRow = ["'©™®'"]; + final expectedRow = ["'©™®'"]; - var result = await connection.query("select t from t"); + final result = await connection.query('select t from t'); expect(result, [expectedRow]); }); - test("UTF16 strings in value with backslash", () async { + test('UTF16 strings in value with backslash', () async { await connection.execute( - "INSERT INTO t (t) values " - "(${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)})", + 'INSERT INTO t (t) values ' + '(${PostgreSQLFormat.id('t', type: PostgreSQLDataType.text)})', substitutionValues: { - "t": "°\\'©™®'", + 't': "°\\'©™®'", }); - var expectedRow = ["°\\'©™®'"]; + final expectedRow = ["°\\'©™®'"]; - var result = await connection.query("select t from t"); + final result = await connection.query('select t from t'); expect(result, [expectedRow]); }); - test("UTF16 strings in query with escape characters", () async { + test('UTF16 strings in query with escape characters', () async { await connection.execute("INSERT INTO t (t) values ('°''©™®''')"); - var expectedRow = ["°'©™®'"]; + final expectedRow = ["°'©™®'"]; - var result = await connection.query("select t from t"); + final result = await connection.query('select t from t'); expect(result, [expectedRow]); }); - test("Really long raw substitution value", () async { - var result = await connection.query( + test('Really long raw substitution value', () async { + final result = await connection.query( "INSERT INTO t (t) VALUES (${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)}) returning t;", - substitutionValues: {"t": lorumIpsum}); + substitutionValues: {'t': lorumIpsum}); expect(result, [ [lorumIpsum] ]); }); - test("Really long SQL string in execute", () async { - var result = await connection + test('Really long SQL string in execute', () async { + final result = await connection .execute("INSERT INTO t (t) VALUES ('$lorumIpsum') returning t;"); expect(result, 1); }); - test("Query without specifying types", () async { + test('Query without specifying types', () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, u) values " - "(${PostgreSQLFormat.id("i")}," - "${PostgreSQLFormat.id("bi")}," - "${PostgreSQLFormat.id("bl")}," - "${PostgreSQLFormat.id("si")}," - "${PostgreSQLFormat.id("t")}," - "${PostgreSQLFormat.id("f")}," - "${PostgreSQLFormat.id("d")}," - "${PostgreSQLFormat.id("dt")}," - "${PostgreSQLFormat.id("ts")}," - "${PostgreSQLFormat.id("tsz")}," - "${PostgreSQLFormat.id("j")}," - "${PostgreSQLFormat.id("u")}" - ") returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u", + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, u) values ' + '(${PostgreSQLFormat.id('i')},' + '${PostgreSQLFormat.id('bi')},' + '${PostgreSQLFormat.id('bl')},' + '${PostgreSQLFormat.id('si')},' + '${PostgreSQLFormat.id('t')},' + '${PostgreSQLFormat.id('f')},' + '${PostgreSQLFormat.id('d')},' + '${PostgreSQLFormat.id('dt')},' + '${PostgreSQLFormat.id('ts')},' + '${PostgreSQLFormat.id('tsz')},' + '${PostgreSQLFormat.id('j')},' + '${PostgreSQLFormat.id('u')}' + ') returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u', substitutionValues: { - "i": 1, - "bi": 2, - "bl": true, - "si": 3, - "t": "foobar", - "f": 5.0, - "d": 6.0, - "dt": new DateTime.utc(2000), - "ts": new DateTime.utc(2000, 2), - "tsz": new DateTime.utc(2000, 3), - "j": {"a": "b"}, - "u": "01234567-89ab-cdef-0123-0123456789ab" + 'i': 1, + 'bi': 2, + 'bl': true, + 'si': 3, + 't': 'foobar', + 'f': 5.0, + 'd': 6.0, + 'dt': DateTime.utc(2000), + 'ts': DateTime.utc(2000, 2), + 'tsz': DateTime.utc(2000, 3), + 'j': {'a': 'b'}, + 'u': '01234567-89ab-cdef-0123-0123456789ab' }); - var expectedRow = [ + final expectedRow = [ 1, 1, 2, 1, true, 3, - "foobar", + 'foobar', 5.0, 6.0, - new DateTime.utc(2000), - new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3), - {"a": "b"}, - "01234567-89ab-cdef-0123-0123456789ab" + DateTime.utc(2000), + DateTime.utc(2000, 2), + DateTime.utc(2000, 3), + {'a': 'b'}, + '01234567-89ab-cdef-0123-0123456789ab' ]; expect(result, [expectedRow]); result = await connection.query( - "select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); + 'select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t'); expect(result, [expectedRow]); }); - test("Query by specifying all types", () async { + test('Query by specifying all types', () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, u) values " - "(${PostgreSQLFormat.id("i", type: PostgreSQLDataType.integer)}," - "${PostgreSQLFormat.id("bi", type: PostgreSQLDataType.bigInteger)}," - "${PostgreSQLFormat.id("bl", type: PostgreSQLDataType.boolean)}," - "${PostgreSQLFormat.id("si", type: PostgreSQLDataType.smallInteger)}," - "${PostgreSQLFormat.id("t", type: PostgreSQLDataType.text)}," - "${PostgreSQLFormat.id("f", type: PostgreSQLDataType.real)}," - "${PostgreSQLFormat.id("d", type: PostgreSQLDataType.double)}," - "${PostgreSQLFormat.id("dt", type: PostgreSQLDataType.date)}," - "${PostgreSQLFormat.id("ts", type: PostgreSQLDataType.timestampWithoutTimezone)}," - "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}," - "${PostgreSQLFormat.id("j", type: PostgreSQLDataType.json)}," - "${PostgreSQLFormat.id("u", type: PostgreSQLDataType.uuid)})" - " returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u", + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, u) values ' + '(${PostgreSQLFormat.id('i', type: PostgreSQLDataType.integer)},' + '${PostgreSQLFormat.id('bi', type: PostgreSQLDataType.bigInteger)},' + '${PostgreSQLFormat.id('bl', type: PostgreSQLDataType.boolean)},' + '${PostgreSQLFormat.id('si', type: PostgreSQLDataType.smallInteger)},' + '${PostgreSQLFormat.id('t', type: PostgreSQLDataType.text)},' + '${PostgreSQLFormat.id('f', type: PostgreSQLDataType.real)},' + '${PostgreSQLFormat.id('d', type: PostgreSQLDataType.double)},' + '${PostgreSQLFormat.id('dt', type: PostgreSQLDataType.date)},' + '${PostgreSQLFormat.id('ts', type: PostgreSQLDataType.timestampWithoutTimezone)},' + '${PostgreSQLFormat.id('tsz', type: PostgreSQLDataType.timestampWithTimezone)},' + '${PostgreSQLFormat.id('j', type: PostgreSQLDataType.json)},' + '${PostgreSQLFormat.id('u', type: PostgreSQLDataType.uuid)})' + ' returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u', substitutionValues: { - "i": 1, - "bi": 2, - "bl": true, - "si": 3, - "t": "foobar", - "f": 5.0, - "d": 6.0, - "dt": new DateTime.utc(2000), - "ts": new DateTime.utc(2000, 2), - "tsz": new DateTime.utc(2000, 3), - "j": {"key": "value"}, - "u": "01234567-89ab-cdef-0123-0123456789ab" + 'i': 1, + 'bi': 2, + 'bl': true, + 'si': 3, + 't': 'foobar', + 'f': 5.0, + 'd': 6.0, + 'dt': DateTime.utc(2000), + 'ts': DateTime.utc(2000, 2), + 'tsz': DateTime.utc(2000, 3), + 'j': {'key': 'value'}, + 'u': '01234567-89ab-cdef-0123-0123456789ab' }); - var expectedRow = [ + final expectedRow = [ 1, 1, 2, 1, true, 3, - "foobar", + 'foobar', 5.0, 6.0, - new DateTime.utc(2000), - new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3), - {"key": "value"}, - "01234567-89ab-cdef-0123-0123456789ab" + DateTime.utc(2000), + DateTime.utc(2000, 2), + DateTime.utc(2000, 3), + {'key': 'value'}, + '01234567-89ab-cdef-0123-0123456789ab' ]; expect(result, [expectedRow]); result = await connection.query( - "select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t"); + 'select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t'); expect(result, [expectedRow]); }); - test("Query by specifying some types", () async { + test('Query by specifying some types', () async { var result = await connection.query( - "INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values " - "(${PostgreSQLFormat.id("i")}," - "${PostgreSQLFormat.id("bi", type: PostgreSQLDataType.bigInteger)}," - "${PostgreSQLFormat.id("bl")}," - "${PostgreSQLFormat.id("si", type: PostgreSQLDataType.smallInteger)}," - "${PostgreSQLFormat.id("t")}," - "${PostgreSQLFormat.id("f", type: PostgreSQLDataType.real)}," - "${PostgreSQLFormat.id("d")}," - "${PostgreSQLFormat.id("dt", type: PostgreSQLDataType.date)}," - "${PostgreSQLFormat.id("ts")}," - "${PostgreSQLFormat.id("tsz", type: PostgreSQLDataType.timestampWithTimezone)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz", + 'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz) values ' + '(${PostgreSQLFormat.id('i')},' + '${PostgreSQLFormat.id('bi', type: PostgreSQLDataType.bigInteger)},' + '${PostgreSQLFormat.id('bl')},' + '${PostgreSQLFormat.id('si', type: PostgreSQLDataType.smallInteger)},' + '${PostgreSQLFormat.id('t')},' + '${PostgreSQLFormat.id('f', type: PostgreSQLDataType.real)},' + '${PostgreSQLFormat.id('d')},' + '${PostgreSQLFormat.id('dt', type: PostgreSQLDataType.date)},' + '${PostgreSQLFormat.id('ts')},' + '${PostgreSQLFormat.id('tsz', type: PostgreSQLDataType.timestampWithTimezone)}) returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz', substitutionValues: { - "i": 1, - "bi": 2, - "bl": true, - "si": 3, - "t": "foobar", - "f": 5.0, - "d": 6.0, - "dt": new DateTime.utc(2000), - "ts": new DateTime.utc(2000, 2), - "tsz": new DateTime.utc(2000, 3), + 'i': 1, + 'bi': 2, + 'bl': true, + 'si': 3, + 't': 'foobar', + 'f': 5.0, + 'd': 6.0, + 'dt': DateTime.utc(2000), + 'ts': DateTime.utc(2000, 2), + 'tsz': DateTime.utc(2000, 3), }); - var expectedRow = [ + final expectedRow = [ 1, 1, 2, 1, true, 3, - "foobar", + 'foobar', 5.0, 6.0, - new DateTime.utc(2000), - new DateTime.utc(2000, 2), - new DateTime.utc(2000, 3) + DateTime.utc(2000), + DateTime.utc(2000, 2), + DateTime.utc(2000, 3) ]; expect(result, [expectedRow]); result = await connection - .query("select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t"); + .query('select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz from t'); expect(result, [expectedRow]); }); - test("Can supply null for values (binary)", () async { - var results = await connection.query( - "INSERT INTO n (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + test('Can supply null for values (binary)', () async { + final results = await connection.query( + 'INSERT INTO n (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2', substitutionValues: { - "i1": null, - "i2": 1, + 'i1': null, + 'i2': 1, }); expect(results, [ @@ -271,12 +271,12 @@ void main() { ]); }); - test("Can supply null for values (text)", () async { - var results = await connection.query( - "INSERT INTO n (i1, i2) values (@i1, @i2:int4) returning i1, i2", + test('Can supply null for values (text)', () async { + final results = await connection.query( + 'INSERT INTO n (i1, i2) values (@i1, @i2:int4) returning i1, i2', substitutionValues: { - "i1": null, - "i2": 1, + 'i1': null, + 'i2': 1, }); expect(results, [ @@ -284,13 +284,13 @@ void main() { ]); }); - test("Overspecifying parameters does not impact query (text)", () async { - var results = await connection.query( - "INSERT INTO u (i1, i2) values (@i1, @i2) returning i1, i2", + test('Overspecifying parameters does not impact query (text)', () async { + final results = await connection.query( + 'INSERT INTO u (i1, i2) values (@i1, @i2) returning i1, i2', substitutionValues: { - "i1": 0, - "i2": 1, - "i3": 0, + 'i1': 0, + 'i2': 1, + 'i3': 0, }); expect(results, [ @@ -298,13 +298,13 @@ void main() { ]); }); - test("Overspecifying parameters does not impact query (binary)", () async { - var results = await connection.query( - "INSERT INTO u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2", + test('Overspecifying parameters does not impact query (binary)', () async { + final results = await connection.query( + 'INSERT INTO u (i1, i2) values (@i1:int4, @i2:int4) returning i1, i2', substitutionValues: { - "i1": 0, - "i2": 1, - "i3": 0, + 'i1': 0, + 'i2': 1, + 'i3': 0, }); expect(results, [ @@ -312,10 +312,10 @@ void main() { ]); }); - test("Can cast text to int on db server", () async { - var results = await connection.query( - "INSERT INTO u (i1, i2) VALUES (@i1::int4, @i2::int4) RETURNING i1, i2", - substitutionValues: {"i1": "0", "i2": "1"}); + test('Can cast text to int on db server', () async { + final results = await connection.query( + 'INSERT INTO u (i1, i2) VALUES (@i1::int4, @i2::int4) RETURNING i1, i2', + substitutionValues: {'i1': '0', 'i2': '1'}); expect(results, [ [0, 1] @@ -323,16 +323,16 @@ void main() { }); }); - group("Unsuccesful queries", () { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + group('Unsuccesful queries', () { + var connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); setUp(() async { - connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await connection.open(); await connection.execute( - "CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)"); + 'CREATE TEMPORARY TABLE t (i1 int not null, i2 int not null)'); }); tearDown(() async { @@ -340,65 +340,65 @@ void main() { }); test( - "A query that fails on the server will report back an exception through the query method", + 'A query that fails on the server will report back an exception through the query method', () async { try { - await connection.query("INSERT INTO t (i1) values (@i1)", - substitutionValues: {"i1": 0}); + await connection.query('INSERT INTO t (i1) values (@i1)', + substitutionValues: {'i1': 0}); expect(true, false); } on PostgreSQLException catch (e) { expect(e.severity, PostgreSQLSeverity.error); - expect(e.message, contains("null value in column \"i2\"")); + expect(e.message, contains('null value in column "i2"')); } }); test( - "Not enough parameters to support format string throws error prior to sending to server", + 'Not enough parameters to support format string throws error prior to sending to server', () async { try { await connection - .query("INSERT INTO t (i1) values (@i1)", substitutionValues: {}); + .query('INSERT INTO t (i1) values (@i1)', substitutionValues: {}); expect(true, false); } on FormatException catch (e) { expect(e.message, - contains("Format string specified identifier with name i1")); + contains('Format string specified identifier with name i1')); } try { - await connection.query("INSERT INTO t (i1) values (@i1)"); + await connection.query('INSERT INTO t (i1) values (@i1)'); expect(true, false); } on FormatException catch (e) { expect(e.message, - contains("Format string specified identifier with name i1")); + contains('Format string specified identifier with name i1')); } }); - test("Wrong type for parameter in substitution values fails", () async { + test('Wrong type for parameter in substitution values fails', () async { try { await connection.query( - "INSERT INTO t (i1, i2) values (@i1:int4, @i2:int4)", - substitutionValues: {"i1": "1", "i2": 1}); + 'INSERT INTO t (i1, i2) values (@i1:int4, @i2:int4)', + substitutionValues: {'i1': '1', 'i2': 1}); expect(true, false); } on FormatException catch (e) { - expect(e.toString(), contains("Invalid type for parameter value")); + expect(e.toString(), contains('Invalid type for parameter value')); } }); - test("Invalid type code", () async { + test('Invalid type code', () async { try { await connection.query( - "INSERT INTO t (i1, i2) values (@i1:qwerty, @i2:int4)", - substitutionValues: {"i1": "1", "i2": 1}); + 'INSERT INTO t (i1, i2) values (@i1:qwerty, @i2:int4)', + substitutionValues: {'i1': '1', 'i2': 1}); expect(true, false); } on FormatException catch (e) { - expect(e.toString(), contains("Invalid type code")); + expect(e.toString(), contains('Invalid type code')); expect(e.toString(), contains("'@i1:qwerty")); } }); }); } -const String lorumIpsum = """Lorem +const String lorumIpsum = '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque in accumsan felis. Nunc semper velit purus, a pellentesque mauris aliquam ut. Sed laoreet iaculis nunc sit amet dignissim. Aenean venenatis sollicitudin @@ -511,4 +511,4 @@ const String lorumIpsum = """Lorem Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin malesuada orci sit amet neque dapibus bibendum. In lobortis imperdiet condimentum. Nullam est nisi, efficitur ac consectetur - eu, efficitur a libero. In nullam."""; + eu, efficitur a libero. In nullam.'''; diff --git a/test/timeout_test.dart b/test/timeout_test.dart index 1d94f9e..21a5f7f 100644 --- a/test/timeout_test.dart +++ b/test/timeout_test.dart @@ -1,15 +1,17 @@ -import 'package:postgres/postgres.dart'; -import 'package:test/test.dart'; import 'dart:async'; +import 'package:test/test.dart'; + +import 'package:postgres/postgres.dart'; + void main() { PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); + await conn.execute('CREATE TEMPORARY TABLE t (id INT UNIQUE)'); }); tearDown(() async { @@ -17,73 +19,73 @@ void main() { }); test( - "Timeout fires on query while in queue does not execute query, query throws exception", + 'Timeout fires on query while in queue does not execute query, query throws exception', () async { //ignore: unawaited_futures - final f = conn.query("SELECT pg_sleep(2)"); + final f = conn.query('SELECT pg_sleep(2)'); try { - await conn.query("SELECT 1", timeoutInSeconds: 1); + await conn.query('SELECT 1', timeoutInSeconds: 1); fail('unreachable'); } on TimeoutException {} expect(f, completes); }); - test("Timeout fires during transaction rolls ack transaction", () async { + test('Timeout fires during transaction rolls ack transaction', () async { try { await conn.transaction((ctx) async { - await ctx.query("INSERT INTO t (id) VALUES (1)"); - await ctx.query("SELECT pg_sleep(2)", timeoutInSeconds: 1); + await ctx.query('INSERT INTO t (id) VALUES (1)'); + await ctx.query('SELECT pg_sleep(2)', timeoutInSeconds: 1); }); fail('unreachable'); } on TimeoutException {} - expect(await conn.query("SELECT * from t"), hasLength(0)); + expect(await conn.query('SELECT * from t'), hasLength(0)); }); test( - "Query on parent context for transaction completes (with error) after timeout", + 'Query on parent context for transaction completes (with error) after timeout', () async { try { await conn.transaction((ctx) async { - await conn.query("SELECT 1", timeoutInSeconds: 1); - await ctx.query("INSERT INTO t (id) VALUES (1)"); + await conn.query('SELECT 1', timeoutInSeconds: 1); + await ctx.query('INSERT INTO t (id) VALUES (1)'); }); fail('unreachable'); } on TimeoutException {} - expect(await conn.query("SELECT * from t"), hasLength(0)); + expect(await conn.query('SELECT * from t'), hasLength(0)); }); test( - "If query is already on the wire and times out, safely throws timeoutexception and nothing else", + 'If query is already on the wire and times out, safely throws timeoutexception and nothing else', () async { try { - await conn.query("SELECT pg_sleep(2)", timeoutInSeconds: 1); + await conn.query('SELECT pg_sleep(2)', timeoutInSeconds: 1); fail('unreachable'); } on TimeoutException {} }); - test("Query times out, next query in the queue runs", () async { + test('Query times out, next query in the queue runs', () async { //ignore: unawaited_futures conn - .query("SELECT pg_sleep(2)", timeoutInSeconds: 1) + .query('SELECT pg_sleep(2)', timeoutInSeconds: 1) .catchError((_) => null); - expect(await conn.query("SELECT 1"), [ + expect(await conn.query('SELECT 1'), [ [1] ]); }); - test("Query that succeeds does not timeout", () async { - await conn.query("SELECT 1", timeoutInSeconds: 1); - expect(new Future.delayed(new Duration(seconds: 2)), completes); + test('Query that succeeds does not timeout', () async { + await conn.query('SELECT 1', timeoutInSeconds: 1); + expect(Future.delayed(Duration(seconds: 2)), completes); }); - test("Query that fails does not timeout", () async { + test('Query that fails does not timeout', () async { await conn .query("INSERT INTO t (id) VALUES ('foo')", timeoutInSeconds: 1) .catchError((_) => null); - expect(new Future.delayed(new Duration(seconds: 2)), completes); + expect(Future.delayed(Duration(seconds: 2)), completes); }); } diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 8cc8785..a409756 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -1,65 +1,67 @@ // ignore_for_file: unawaited_futures +import 'dart:async'; -import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; -import 'dart:async'; + +import 'package:postgres/postgres.dart'; void main() { - group("Transaction behavior", () { - PostgreSQLConnection conn = null; + group('Transaction behavior', () { + PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); + await conn.execute('CREATE TEMPORARY TABLE t (id INT UNIQUE)'); }); tearDown(() async { await conn?.close(); }); - test("Rows are Lists of column values", () async { - await conn.execute("INSERT INTO t (id) VALUES (1)"); + test('Rows are Lists of column values', () async { + await conn.execute('INSERT INTO t (id) VALUES (1)'); - final List> outValue = await conn.transaction((ctx) async { + final outValue = await conn.transaction((ctx) async { return await ctx.query('SELECT * FROM t WHERE id = @id LIMIT 1', substitutionValues: {'id': 1}); - }); + }) as List; expect(outValue.length, 1); expect(outValue.first is List, true); - expect(outValue.first.length, 1); - expect(outValue.first.first, 1); + final firstItem = outValue.first as List; + expect(firstItem.length, 1); + expect(firstItem.first, 1); }); - test("Send successful transaction succeeds, returns returned value", + test('Send successful transaction succeeds, returns returned value', () async { - var outResult = await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + final outResult = await conn.transaction((c) async { + await c.query('INSERT INTO t (id) VALUES (1)'); - return await c.query("SELECT id FROM t"); + return await c.query('SELECT id FROM t'); }); expect(outResult, [ [1] ]); - var result = await conn.query("SELECT id FROM t"); + final result = await conn.query('SELECT id FROM t'); expect(result, [ [1] ]); }); - test("Query during transaction must wait until transaction is finished", + test('Query during transaction must wait until transaction is finished', () async { - var orderEnsurer = []; - var nextCompleter = new Completer.sync(); - var outResult = conn.transaction((c) async { + final orderEnsurer = []; + final nextCompleter = Completer.sync(); + final outResult = conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); nextCompleter.complete(); - var result = await c.query("SELECT id FROM t"); + final result = await c.query('SELECT id FROM t'); orderEnsurer.add(3); return result; @@ -67,12 +69,12 @@ void main() { await nextCompleter.future; orderEnsurer.add(11); - await conn.query("INSERT INTO t (id) VALUES (2)"); + await conn.query('INSERT INTO t (id) VALUES (2)'); orderEnsurer.add(12); - var laterResults = await conn.query("SELECT id FROM t"); + final laterResults = await conn.query('SELECT id FROM t'); orderEnsurer.add(13); - var firstResult = await outResult; + final firstResult = await outResult; expect(orderEnsurer, [1, 2, 11, 3, 12, 13]); expect(firstResult, [ @@ -84,32 +86,32 @@ void main() { ]); }); - test("Make sure two simultaneous transactions cannot be interwoven", + test('Make sure two simultaneous transactions cannot be interwoven', () async { - var orderEnsurer = []; + final orderEnsurer = []; - var firstTransactionFuture = conn.transaction((c) async { + final firstTransactionFuture = conn.transaction((c) async { orderEnsurer.add(11); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(12); - var result = await c.query("SELECT id FROM t"); + final result = await c.query('SELECT id FROM t'); orderEnsurer.add(13); return result; }); - var secondTransactionFuture = conn.transaction((c) async { + final secondTransactionFuture = conn.transaction((c) async { orderEnsurer.add(21); - await c.query("INSERT INTO t (id) VALUES (2)"); + await c.query('INSERT INTO t (id) VALUES (2)'); orderEnsurer.add(22); - var result = await c.query("SELECT id FROM t"); + final result = await c.query('SELECT id FROM t'); orderEnsurer.add(23); return result; }); - var firstResults = await firstTransactionFuture; - var secondResults = await secondTransactionFuture; + final firstResults = await firstTransactionFuture; + final secondResults = await secondTransactionFuture; expect(orderEnsurer, [11, 12, 13, 21, 22, 23]); @@ -122,37 +124,37 @@ void main() { ]); }); - test("May intentionally rollback transaction", () async { - var reached = false; + test('May intentionally rollback transaction', () async { + bool reached = false; await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); c.cancelTransaction(); reached = true; - await c.query("INSERT INTO t (id) VALUES (2)"); + await c.query('INSERT INTO t (id) VALUES (2)'); }); expect(reached, false); - var result = await conn.query("SELECT id FROM t"); + final result = await conn.query('SELECT id FROM t'); expect(result, []); }); - test("Intentional rollback on non-transaction has no impact", () async { + test('Intentional rollback on non-transaction has no impact', () async { conn.cancelTransaction(); - var result = await conn.query("SELECT id FROM t"); + final result = await conn.query('SELECT id FROM t'); expect(result, []); }); - test("Intentional rollback from outside of a transaction has no impact", + test('Intentional rollback from outside of a transaction has no impact', () async { - var orderEnsurer = []; - var nextCompleter = new Completer.sync(); - var outResult = conn.transaction((c) async { + final orderEnsurer = []; + final nextCompleter = Completer.sync(); + final outResult = conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); nextCompleter.complete(); - var result = await c.query("SELECT id FROM t"); + final result = await c.query('SELECT id FROM t'); orderEnsurer.add(3); return result; @@ -162,7 +164,7 @@ void main() { conn.cancelTransaction(); orderEnsurer.add(11); - var results = await outResult; + final results = await outResult; expect(orderEnsurer, [1, 2, 11, 3]); expect(results, [ @@ -170,15 +172,15 @@ void main() { ]); }); - test("A transaction does not preempt pending queries", () async { + test('A transaction does not preempt pending queries', () async { // Add a few insert queries but don't await, then do a transaction that does a fetch, // make sure that transaction contains all of the elements. - conn.execute("INSERT INTO t (id) VALUES (1)"); - conn.execute("INSERT INTO t (id) VALUES (2)"); - conn.execute("INSERT INTO t (id) VALUES (3)"); + conn.execute('INSERT INTO t (id) VALUES (1)'); + conn.execute('INSERT INTO t (id) VALUES (2)'); + conn.execute('INSERT INTO t (id) VALUES (3)'); - var results = await conn.transaction((ctx) async { - return await ctx.query("SELECT id FROM t"); + final results = await conn.transaction((ctx) async { + return await ctx.query('SELECT id FROM t'); }); expect(results, [ [1], @@ -189,12 +191,12 @@ void main() { test("A transaction doesn't have to await on queries", () async { conn.transaction((ctx) async { - ctx.query("INSERT INTO t (id) VALUES (1)"); - ctx.query("INSERT INTO t (id) VALUES (2)"); - ctx.query("INSERT INTO t (id) VALUES (3)"); + ctx.query('INSERT INTO t (id) VALUES (1)'); + ctx.query('INSERT INTO t (id) VALUES (2)'); + ctx.query('INSERT INTO t (id) VALUES (3)'); }); - var total = await conn.query("SELECT id FROM t"); + final total = await conn.query('SELECT id FROM t'); expect(total, [ [1], [2], @@ -205,90 +207,86 @@ void main() { test( "A transaction doesn't have to await on queries, when the last query fails, it still emits an error from the transaction", () async { - var transactionError; + dynamic transactionError; await conn.transaction((ctx) async { - ctx.query("INSERT INTO t (id) VALUES (1)"); - ctx.query("INSERT INTO t (id) VALUES (2)"); + ctx.query('INSERT INTO t (id) VALUES (1)'); + ctx.query('INSERT INTO t (id) VALUES (2)'); ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((e) {}); }).catchError((e) => transactionError = e); expect(transactionError, isNotNull); - var total = await conn.query("SELECT id FROM t"); + final total = await conn.query('SELECT id FROM t'); expect(total, []); }); test( "A transaction doesn't have to await on queries, when the non-last query fails, it still emits an error from the transaction", () async { - var failingQueryError; - var pendingQueryError; - var transactionError; + dynamic failingQueryError; + dynamic pendingQueryError; + dynamic transactionError; await conn.transaction((ctx) async { - ctx.query("INSERT INTO t (id) VALUES (1)"); + ctx.query('INSERT INTO t (id) VALUES (1)'); ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((e) { failingQueryError = e; }); - ctx.query("INSERT INTO t (id) VALUES (2)").catchError((e) { + ctx.query('INSERT INTO t (id) VALUES (2)').catchError((e) { pendingQueryError = e; }); }).catchError((e) => transactionError = e); expect(transactionError, isNotNull); - expect(failingQueryError.toString(), contains("invalid input")); + expect(failingQueryError.toString(), contains('invalid input')); expect( - pendingQueryError.toString(), contains("failed prior to execution")); - var total = await conn.query("SELECT id FROM t"); + pendingQueryError.toString(), contains('failed prior to execution')); + final total = await conn.query('SELECT id FROM t'); expect(total, []); }); test( - "A transaction with a rollback and non-await queries rolls back transaction", + 'A transaction with a rollback and non-await queries rolls back transaction', () async { - var errs = []; + final errs = []; await conn.transaction((ctx) async { - ctx.query("INSERT INTO t (id) VALUES (1)").catchError((e) { - errs.add(e); - }); - ctx.query("INSERT INTO t (id) VALUES (2)").catchError((e) { - errs.add(e); - }); + ctx.query('INSERT INTO t (id) VALUES (1)').catchError(errs.add); + ctx.query('INSERT INTO t (id) VALUES (2)').catchError(errs.add); ctx.cancelTransaction(); - ctx.query("INSERT INTO t (id) VALUES (3)").catchError((e) {}); + ctx.query('INSERT INTO t (id) VALUES (3)').catchError((e) {}); }); - var total = await conn.query("SELECT id FROM t"); + final total = await conn.query('SELECT id FROM t'); expect(total, []); expect(errs.length, 2); }); test( - "A transaction that mixes awaiting and non-awaiting queries fails gracefully when an awaited query fails", + 'A transaction that mixes awaiting and non-awaiting queries fails gracefully when an awaited query fails', () async { - var transactionError; + dynamic transactionError; await conn.transaction((ctx) async { - ctx.query("INSERT INTO t (id) VALUES (1)"); + ctx.query('INSERT INTO t (id) VALUES (1)'); await ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((_) {}); - ctx.query("INSERT INTO t (id) VALUES (2)").catchError((_) {}); + ctx.query('INSERT INTO t (id) VALUES (2)').catchError((_) {}); }).catchError((e) => transactionError = e); expect(transactionError, isNotNull); - var total = await conn.query("SELECT id FROM t"); + final total = await conn.query('SELECT id FROM t'); expect(total, []); }); test( - "A transaction that mixes awaiting and non-awaiting queries fails gracefully when an unawaited query fails", + 'A transaction that mixes awaiting and non-awaiting queries fails gracefully when an unawaited query fails', () async { - var transactionError; + dynamic transactionError; await conn.transaction((ctx) async { - await ctx.query("INSERT INTO t (id) VALUES (1)"); + await ctx.query('INSERT INTO t (id) VALUES (1)'); ctx.query("INSERT INTO t (id) VALUES ('foo')").catchError((_) {}); - await ctx.query("INSERT INTO t (id) VALUES (2)").catchError((_) {}); + await ctx.query('INSERT INTO t (id) VALUES (2)').catchError((_) {}); }).catchError((e) => transactionError = e); expect(transactionError, isNotNull); - var total = await conn.query("SELECT id FROM t"); + final total = await conn.query('SELECT id FROM t'); expect(total, []); }); }); @@ -296,76 +294,76 @@ void main() { // A transaction can fail for three reasons: query error, exception in code, or a rollback. // After a transaction fails, the changes must be rolled back, it should continue with pending queries, pending transactions, later queries, later transactions - group("Transaction:Query recovery", () { - PostgreSQLConnection conn = null; + group('Transaction:Query recovery', () { + PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); + await conn.execute('CREATE TEMPORARY TABLE t (id INT UNIQUE)'); }); tearDown(() async { await conn?.close(); }); - test("Is rolled back/executes later query", () async { + test('Is rolled back/executes later query', () async { try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); - var oneRow = await c.query("SELECT id FROM t"); + await c.query('INSERT INTO t (id) VALUES (1)'); + final oneRow = await c.query('SELECT id FROM t'); expect(oneRow, [ [1] ]); // This will error - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); }); expect(true, false); } on PostgreSQLException catch (e) { - expect(e.message, contains("unique constraint")); + expect(e.message, contains('unique constraint')); } - var noRows = await conn.query("SELECT id FROM t"); + final noRows = await conn.query('SELECT id FROM t'); expect(noRows, []); }); - test("Executes pending query", () async { - var orderEnsurer = []; + test('Executes pending query', () async { + final orderEnsurer = []; conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); // This will error - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); }).catchError((e) => null); orderEnsurer.add(11); - var result = await conn.query("SELECT id FROM t"); + final result = await conn.query('SELECT id FROM t'); orderEnsurer.add(12); expect(orderEnsurer, [11, 1, 2, 12]); expect(result, []); }); - test("Executes pending transaction", () async { - var orderEnsurer = []; + test('Executes pending transaction', () async { + final orderEnsurer = []; conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); // This will error - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); }).catchError((e) => null); - var result = await conn.transaction((ctx) async { + final result = await conn.transaction((ctx) async { orderEnsurer.add(11); - return await ctx.query("SELECT id FROM t"); + return await ctx.query('SELECT id FROM t'); }); orderEnsurer.add(12); @@ -373,86 +371,86 @@ void main() { expect(result, []); }); - test("Executes later transaction", () async { + test('Executes later transaction', () async { try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); - var oneRow = await c.query("SELECT id FROM t"); + await c.query('INSERT INTO t (id) VALUES (1)'); + final oneRow = await c.query('SELECT id FROM t'); expect(oneRow, [ [1] ]); // This will error - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); }); expect(true, false); } on PostgreSQLException {} - var result = await conn.transaction((ctx) async { - return await ctx.query("SELECT id FROM t"); + final result = await conn.transaction((ctx) async { + return await ctx.query('SELECT id FROM t'); }); expect(result, []); }); }); - group("Transaction:Exception recovery", () { - PostgreSQLConnection conn = null; + group('Transaction:Exception recovery', () { + PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); + await conn.execute('CREATE TEMPORARY TABLE t (id INT UNIQUE)'); }); tearDown(() async { await conn?.close(); }); - test("Is rolled back/executes later query", () async { + test('Is rolled back/executes later query', () async { try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); - throw 'foo'; + await c.query('INSERT INTO t (id) VALUES (1)'); + throw Exception('foo'); }); expect(true, false); - } on String {} + } on Exception {} - var noRows = await conn.query("SELECT id FROM t"); + final noRows = await conn.query('SELECT id FROM t'); expect(noRows, []); }); - test("Executes pending query", () async { - var orderEnsurer = []; + test('Executes pending query', () async { + final orderEnsurer = []; conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); - throw 'foo'; + throw Exception('foo'); }).catchError((e) => null); orderEnsurer.add(11); - var result = await conn.query("SELECT id FROM t"); + final result = await conn.query('SELECT id FROM t'); orderEnsurer.add(12); expect(orderEnsurer, [11, 1, 2, 12]); expect(result, []); }); - test("Executes pending transaction", () async { - var orderEnsurer = []; + test('Executes pending transaction', () async { + final orderEnsurer = []; conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); - throw 'foo'; + throw Exception('foo'); }).catchError((e) => null); - var result = await conn.transaction((ctx) async { + final result = await conn.transaction((ctx) async { orderEnsurer.add(11); - return await ctx.query("SELECT id FROM t"); + return await ctx.query('SELECT id FROM t'); }); orderEnsurer.add(12); @@ -460,140 +458,140 @@ void main() { expect(result, []); }); - test("Executes later transaction", () async { + test('Executes later transaction', () async { try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); - throw 'foo'; + await c.query('INSERT INTO t (id) VALUES (1)'); + throw Exception('foo'); }); expect(true, false); - } on String {} + } on Exception {} - var result = await conn.transaction((ctx) async { - return await ctx.query("SELECT id FROM t"); + final result = await conn.transaction((ctx) async { + return await ctx.query('SELECT id FROM t'); }); expect(result, []); }); test( - "If exception thrown while preparing query, transaction gets rolled back", + 'If exception thrown while preparing query, transaction gets rolled back', () async { try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); - c.query("INSERT INTO t (id) VALUES (@id:int4)", - substitutionValues: {"id": "foobar"}).catchError((_) => null); - await c.query("INSERT INTO t (id) VALUES (2)"); + c.query('INSERT INTO t (id) VALUES (@id:int4)', + substitutionValues: {'id': 'foobar'}).catchError((_) => null); + await c.query('INSERT INTO t (id) VALUES (2)'); }); expect(true, false); } catch (e) { expect(e is FormatException, true); } - var noRows = await conn.query("SELECT id FROM t"); + final noRows = await conn.query('SELECT id FROM t'); expect(noRows, []); }); - test("Async query failure prevents closure from continuning", () async { - var reached = false; + test('Async query failure prevents closure from continuning', () async { + bool reached = false; try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); await c.query("INSERT INTO t (id) VALUE ('foo') RETURNING id"); reached = true; - await c.query("INSERT INTO t (id) VALUES (2)"); + await c.query('INSERT INTO t (id) VALUES (2)'); }); fail('unreachable'); } on PostgreSQLException {} expect(reached, false); - final res = await conn.query("SELECT * FROM t"); + final res = await conn.query('SELECT * FROM t'); expect(res, []); }); test( - "When exception thrown in unawaited on future, transaction is rolled back", + 'When exception thrown in unawaited on future, transaction is rolled back', () async { try { await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); c .query("INSERT INTO t (id) VALUE ('foo') RETURNING id") .catchError((_) => null); - await c.query("INSERT INTO t (id) VALUES (2)"); + await c.query('INSERT INTO t (id) VALUES (2)'); }); fail('unreachable'); } on PostgreSQLException {} - final res = await conn.query("SELECT * FROM t"); + final res = await conn.query('SELECT * FROM t'); expect(res, []); }); }); - group("Transaction:Rollback recovery", () { - PostgreSQLConnection conn = null; + group('Transaction:Rollback recovery', () { + PostgreSQLConnection conn; setUp(() async { - conn = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); + conn = PostgreSQLConnection('localhost', 5432, 'dart_test', + username: 'dart', password: 'dart'); await conn.open(); - await conn.execute("CREATE TEMPORARY TABLE t (id INT UNIQUE)"); + await conn.execute('CREATE TEMPORARY TABLE t (id INT UNIQUE)'); }); tearDown(() async { await conn?.close(); }); - test("Is rolled back/executes later query", () async { - var result = await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + test('Is rolled back/executes later query', () async { + final result = await conn.transaction((c) async { + await c.query('INSERT INTO t (id) VALUES (1)'); c.cancelTransaction(); - await c.query("INSERT INTO t (id) VALUES (2)"); + await c.query('INSERT INTO t (id) VALUES (2)'); }); expect(result is PostgreSQLRollback, true); - var noRows = await conn.query("SELECT id FROM t"); + final noRows = await conn.query('SELECT id FROM t'); expect(noRows, []); }); - test("Executes pending query", () async { - var orderEnsurer = []; + test('Executes pending query', () async { + final orderEnsurer = []; conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); - await c.cancelTransaction(); - await c.query("INSERT INTO t (id) VALUES (2)"); + c.cancelTransaction(); + await c.query('INSERT INTO t (id) VALUES (2)'); }); orderEnsurer.add(11); - var result = await conn.query("SELECT id FROM t"); + final result = await conn.query('SELECT id FROM t'); orderEnsurer.add(12); expect(orderEnsurer, [11, 1, 2, 12]); expect(result, []); }); - test("Executes pending transaction", () async { - var orderEnsurer = []; + test('Executes pending transaction', () async { + final orderEnsurer = []; conn.transaction((c) async { orderEnsurer.add(1); - await c.query("INSERT INTO t (id) VALUES (1)"); + await c.query('INSERT INTO t (id) VALUES (1)'); orderEnsurer.add(2); - await c.cancelTransaction(); - await c.query("INSERT INTO t (id) VALUES (2)"); + c.cancelTransaction(); + await c.query('INSERT INTO t (id) VALUES (2)'); orderEnsurer.add(3); }); - var result = await conn.transaction((ctx) async { + final result = await conn.transaction((ctx) async { orderEnsurer.add(11); - return await ctx.query("SELECT id FROM t"); + return await ctx.query('SELECT id FROM t'); }); orderEnsurer.add(12); @@ -601,16 +599,16 @@ void main() { expect(result, []); }); - test("Executes later transaction", () async { - var result = await conn.transaction((c) async { - await c.query("INSERT INTO t (id) VALUES (1)"); + test('Executes later transaction', () async { + dynamic result = await conn.transaction((c) async { + await c.query('INSERT INTO t (id) VALUES (1)'); c.cancelTransaction(); - await c.query("INSERT INTO t (id) VALUES (2)"); + await c.query('INSERT INTO t (id) VALUES (2)'); }); expect(result is PostgreSQLRollback, true); result = await conn.transaction((ctx) async { - return await ctx.query("SELECT id FROM t"); + return await ctx.query('SELECT id FROM t'); }); expect(result, []); }); From a83e930c2b0061e7513350bdf2627c215f750966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 23 Mar 2019 11:08:38 +0100 Subject: [PATCH 46/73] Misc code update (#82) Misc code hardening. --- lib/src/binary_codec.dart | 6 +- lib/src/client_messages.dart | 124 +++++++++++++++------------------ lib/src/connection.dart | 4 +- lib/src/constants.dart | 10 +-- lib/src/query.dart | 9 +-- lib/src/query_cache.dart | 17 +++-- lib/src/query_queue.dart | 4 +- lib/src/substituter.dart | 2 +- lib/src/text_codec.dart | 6 +- lib/src/transaction_proxy.dart | 23 +++--- test/query_reuse_test.dart | 21 +++--- 11 files changed, 109 insertions(+), 117 deletions(-) diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index 2108663..541854c 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -7,9 +7,9 @@ import '../postgres.dart'; import 'types.dart'; class PostgresBinaryEncoder extends Converter { - const PostgresBinaryEncoder(this.dataType); + final PostgreSQLDataType _dataType; - final PostgreSQLDataType dataType; + const PostgresBinaryEncoder(this._dataType); @override Uint8List convert(dynamic value) { @@ -17,7 +17,7 @@ class PostgresBinaryEncoder extends Converter { return null; } - switch (dataType) { + switch (_dataType) { case PostgreSQLDataType.boolean: { if (value is bool) { diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index f46a49f..f737953 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -48,26 +48,21 @@ abstract class ClientMessage { } class StartupMessage extends ClientMessage { - StartupMessage(String databaseName, String timeZone, {String username}) { - this.databaseName = UTF8BackedString(databaseName); - this.timeZone = UTF8BackedString(timeZone); - if (username != null) { - this.username = UTF8BackedString(username); - } - } + final UTF8BackedString _username; + final UTF8BackedString _databaseName; + final UTF8BackedString _timeZone; - UTF8BackedString username; - UTF8BackedString databaseName; - UTF8BackedString timeZone; - - ByteData buffer; + StartupMessage(String databaseName, String timeZone, {String username}) + : _databaseName = UTF8BackedString(databaseName), + _timeZone = UTF8BackedString(timeZone), + _username = username == null ? null : UTF8BackedString(username); @override int get length { final fixedLength = 53; - final variableLength = (username?.utf8Length ?? 0) + - databaseName.utf8Length + - timeZone.utf8Length + + final variableLength = (_username?.utf8Length ?? 0) + + _databaseName.utf8Length + + _timeZone.utf8Length + 3; return fixedLength + variableLength; @@ -78,80 +73,78 @@ class StartupMessage extends ClientMessage { buffer.writeInt32(length); buffer.writeInt32(ClientMessage.ProtocolVersion); - if (username != null) { + if (_username != null) { applyBytesToBuffer((UTF8ByteConstants.user), buffer); - applyStringToBuffer(username, buffer); + applyStringToBuffer(_username, buffer); } applyBytesToBuffer(UTF8ByteConstants.database, buffer); - applyStringToBuffer(databaseName, buffer); + applyStringToBuffer(_databaseName, buffer); applyBytesToBuffer(UTF8ByteConstants.clientEncoding, buffer); applyBytesToBuffer(UTF8ByteConstants.utf8, buffer); applyBytesToBuffer(UTF8ByteConstants.timeZone, buffer); - applyStringToBuffer(timeZone, buffer); + applyStringToBuffer(_timeZone, buffer); buffer.writeInt8(0); } } class AuthMD5Message extends ClientMessage { + UTF8BackedString _hashedAuthString; + AuthMD5Message(String username, String password, List saltBytes) { final passwordHash = md5.convert('$password$username'.codeUnits).toString(); final saltString = String.fromCharCodes(saltBytes); final md5Hash = md5.convert('$passwordHash$saltString'.codeUnits).toString(); - hashedAuthString = UTF8BackedString('md5$md5Hash'); + _hashedAuthString = UTF8BackedString('md5$md5Hash'); } - UTF8BackedString hashedAuthString; - @override int get length { - return 6 + hashedAuthString.utf8Length; + return 6 + _hashedAuthString.utf8Length; } @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.PasswordIdentifier); buffer.writeUint32(length - 1); - applyStringToBuffer(hashedAuthString, buffer); + applyStringToBuffer(_hashedAuthString, buffer); } } class QueryMessage extends ClientMessage { - QueryMessage(String queryString) { - this.queryString = UTF8BackedString(queryString); - } + final UTF8BackedString _queryString; - UTF8BackedString queryString; + QueryMessage(String queryString) + : _queryString = UTF8BackedString(queryString); @override int get length { - return 6 + queryString.utf8Length; + return 6 + _queryString.utf8Length; } @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.QueryIdentifier); buffer.writeUint32(length - 1); - applyStringToBuffer(queryString, buffer); + applyStringToBuffer(_queryString, buffer); } } class ParseMessage extends ClientMessage { - ParseMessage(String statement, {String statementName = ''}) { - this.statement = UTF8BackedString(statement); - this.statementName = UTF8BackedString(statementName); - } + final UTF8BackedString _statementName; + final UTF8BackedString _statement; - UTF8BackedString statementName; - UTF8BackedString statement; + ParseMessage(String statement, {String statementName = ''}) + : _statement = UTF8BackedString(statement), + _statementName = UTF8BackedString(statementName); @override int get length { - return 9 + statement.utf8Length + statementName.utf8Length; + return 9 + _statement.utf8Length + _statementName.utf8Length; } @override @@ -159,22 +152,21 @@ class ParseMessage extends ClientMessage { buffer.writeUint8(ClientMessage.ParseIdentifier); buffer.writeUint32(length - 1); // Name of prepared statement - applyStringToBuffer(statementName, buffer); - applyStringToBuffer(statement, buffer); // Query string + applyStringToBuffer(_statementName, buffer); + applyStringToBuffer(_statement, buffer); // Query string buffer.writeUint16(0); } } class DescribeMessage extends ClientMessage { - DescribeMessage({String statementName = ''}) { - this.statementName = UTF8BackedString(statementName); - } + final UTF8BackedString _statementName; - UTF8BackedString statementName; + DescribeMessage({String statementName = ''}) + : _statementName = UTF8BackedString(statementName); @override int get length { - return 7 + statementName.utf8Length; + return 7 + _statementName.utf8Length; } @override @@ -182,35 +174,33 @@ class DescribeMessage extends ClientMessage { buffer.writeUint8(ClientMessage.DescribeIdentifier); buffer.writeUint32(length - 1); buffer.writeUint8(83); - applyStringToBuffer(statementName, buffer); // Name of prepared statement + applyStringToBuffer(_statementName, buffer); // Name of prepared statement } } class BindMessage extends ClientMessage { - BindMessage(this.parameters, {String statementName = ''}) { - typeSpecCount = parameters.where((p) => p.isBinary).length; - this.statementName = UTF8BackedString(statementName); - } - - List parameters; - UTF8BackedString statementName; - - int typeSpecCount; + final List _parameters; + final UTF8BackedString _statementName; + final int _typeSpecCount; int _cachedLength; + BindMessage(this._parameters, {String statementName = ''}) + : _typeSpecCount = _parameters.where((p) => p.isBinary).length, + _statementName = UTF8BackedString(statementName); + @override int get length { if (_cachedLength == null) { - var inputParameterElementCount = parameters.length; - if (typeSpecCount == parameters.length || typeSpecCount == 0) { + var inputParameterElementCount = _parameters.length; + if (_typeSpecCount == _parameters.length || _typeSpecCount == 0) { inputParameterElementCount = 1; } _cachedLength = 15; - _cachedLength += statementName.utf8Length; + _cachedLength += _statementName.utf8Length; _cachedLength += inputParameterElementCount * 2; _cachedLength += - parameters.fold(0, (len, ParameterValue paramValue) { + _parameters.fold(0, (len, ParameterValue paramValue) { if (paramValue.bytes == null) { return len + 4; } else { @@ -229,30 +219,30 @@ class BindMessage extends ClientMessage { // Name of portal - currently unnamed portal. applyBytesToBuffer([0], buffer); // Name of prepared statement. - applyStringToBuffer(statementName, buffer); + applyStringToBuffer(_statementName, buffer); // OK, if we have no specified types at all, we can use 0. If we have all specified types, we can use 1. If we have a mix, we have to individually // call out each type. - if (typeSpecCount == parameters.length) { + if (_typeSpecCount == _parameters.length) { buffer.writeUint16(1); // Apply following format code for all parameters by indicating 1 buffer.writeUint16(ClientMessage.FormatBinary); - } else if (typeSpecCount == 0) { + } else if (_typeSpecCount == 0) { buffer.writeUint16(1); // Apply following format code for all parameters by indicating 1 buffer.writeUint16(ClientMessage.FormatText); } else { // Well, we have some text and some binary, so we have to be explicit about each one - buffer.writeUint16(parameters.length); - parameters.forEach((p) { + buffer.writeUint16(_parameters.length); + _parameters.forEach((p) { buffer.writeUint16( p.isBinary ? ClientMessage.FormatBinary : ClientMessage.FormatText); }); } // This must be the number of $n's in the query. - buffer.writeUint16(parameters.length); - parameters.forEach((p) { + buffer.writeUint16(_parameters.length); + _parameters.forEach((p) { if (p.bytes == null) { buffer.writeInt32(-1); } else { @@ -268,8 +258,6 @@ class BindMessage extends ClientMessage { } class ExecuteMessage extends ClientMessage { - ExecuteMessage(); - @override int get length { return 10; @@ -285,8 +273,6 @@ class ExecuteMessage extends ClientMessage { } class SyncMessage extends ClientMessage { - SyncMessage(); - @override int get length { return 5; diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 3b1eefd..c67a379 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -205,9 +205,9 @@ class PostgreSQLConnection extends Object final proxy = _TransactionProxy(this, queryBlock, commitTimeoutInSeconds); - await _enqueue(proxy.beginQuery); + await _enqueue(proxy._beginQuery); - return await proxy.completer.future; + return await proxy.future; } @override diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 1364c63..4d7d872 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,7 +1,7 @@ class UTF8ByteConstants { - static const user = const [117, 115, 101, 114, 0]; - static const database = const [100, 97, 116, 97, 98, 97, 115, 101, 0]; - static const clientEncoding = const [ + static const user = const [117, 115, 101, 114, 0]; + static const database = const [100, 97, 116, 97, 98, 97, 115, 101, 0]; + static const clientEncoding = const [ 99, 108, 105, @@ -19,6 +19,6 @@ class UTF8ByteConstants { 103, 0 ]; - static const utf8 = const [85, 84, 70, 56, 0]; - static const timeZone = const [84, 105, 109, 101, 90, 111, 110, 101, 0]; + static const utf8 = const [85, 84, 70, 56, 0]; + static const timeZone = const [84, 105, 109, 101, 90, 111, 110, 101, 0]; } diff --git a/lib/src/query.dart b/lib/src/query.dart index b76247b..21d49f1 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -28,8 +28,8 @@ class Query { final PostgreSQLExecutionContext transaction; final PostgreSQLConnection connection; - List specifiedParameterTypeCodes; - List> rows = []; + List _specifiedParameterTypeCodes; + final rows = >[]; CachedQuery cache; @@ -68,7 +68,8 @@ class Query { return '\$$index'; }); - specifiedParameterTypeCodes = formatIdentifiers.map((i) => i.type).toList(); + _specifiedParameterTypeCodes = + formatIdentifiers.map((i) => i.type).toList(); final parameterList = formatIdentifiers .map((id) => ParameterValue(id, substitutionValues)) @@ -108,7 +109,7 @@ class Query { PostgreSQLException validateParameters(List parameterTypeIDs) { final actualParameterTypeCodeIterator = parameterTypeIDs.iterator; final parametersAreMismatched = - specifiedParameterTypeCodes.map((specifiedType) { + _specifiedParameterTypeCodes.map((specifiedType) { actualParameterTypeCodeIterator.moveNext(); if (specifiedType == null) { diff --git a/lib/src/query_cache.dart b/lib/src/query_cache.dart index 221c8f9..2acf5d9 100644 --- a/lib/src/query_cache.dart +++ b/lib/src/query_cache.dart @@ -1,8 +1,11 @@ import 'query.dart'; class QueryCache { - final Map queries = {}; - int idCounter = 0; + final Map _queries = {}; + int _idCounter = 0; + + int get length => _queries.length; + bool get isEmpty => _queries.isEmpty; void add(Query query) { if (query.cache == null) { @@ -10,7 +13,7 @@ class QueryCache { } if (query.cache.isValid) { - queries[query.statement] = query.cache; + _queries[query.statement] = query.cache; } } @@ -19,18 +22,18 @@ class QueryCache { return null; } - return queries[statementId]; + return _queries[statementId]; } String identifierForQuery(Query query) { - final existing = queries[query.statement]; + final existing = _queries[query.statement]; if (existing != null) { return existing.preparedStatementName; } - final string = '$idCounter'.padLeft(12, '0'); + final string = '$_idCounter'.padLeft(12, '0'); - idCounter++; + _idCounter++; return string; } diff --git a/lib/src/query_queue.dart b/lib/src/query_queue.dart index 1007d84..29514cb 100644 --- a/lib/src/query_queue.dart +++ b/lib/src/query_queue.dart @@ -7,7 +7,7 @@ import 'query.dart'; class QueryQueue extends ListBase> implements List> { - List> _inner = []; + List> _inner = >[]; bool _isCancelled = false; PostgreSQLException get _cancellationException => PostgreSQLException( @@ -24,7 +24,7 @@ class QueryQueue extends ListBase> _isCancelled = true; error ??= _cancellationException; final existing = _inner; - _inner = []; + _inner = >[]; // We need to jump this to the next event so that the queries // get the error and not the close message, since completeError is diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 41505f7..35ce77b 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -55,7 +55,7 @@ class PostgreSQLFormat { static String substitute(String fmtString, Map values, {SQLReplaceIdentifierFunction replace}) { final converter = PostgresTextEncoder(true); - values ??= {}; + values ??= {}; replace ??= (spec, index) => converter.convert(values[spec.name]); final items = []; diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index f85516e..215f6a4 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:postgres/postgres.dart'; class PostgresTextEncoder extends Converter { - const PostgresTextEncoder(this.escapeStrings); + const PostgresTextEncoder(this._escapeStrings); - final bool escapeStrings; + final bool _escapeStrings; @override String convert(dynamic value) { @@ -22,7 +22,7 @@ class PostgresTextEncoder extends Converter { } if (value is String) { - return encodeString(value, escapeStrings); + return encodeString(value, _escapeStrings); } if (value is DateTime) { diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index df530c3..eff2e8d 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -8,20 +8,20 @@ class _TransactionProxy extends Object implements PostgreSQLExecutionContext { _TransactionProxy( this._connection, this.executionBlock, this.commitTimeoutInSeconds) { - beginQuery = Query('BEGIN', {}, _connection, this) + _beginQuery = Query('BEGIN', {}, _connection, this) ..onlyReturnAffectedRowCount = true; - beginQuery.future.then(startTransaction).catchError((err, StackTrace st) { + _beginQuery.future.then(startTransaction).catchError((err, StackTrace st) { Future(() { - completer.completeError(err, st); + _completer.completeError(err, st); }); }); } - Query beginQuery; - Completer completer = Completer(); + Query _beginQuery; + final _completer = Completer(); - Future get future => completer.future; + Future get future => _completer.future; @override final PostgreSQLConnection _connection; @@ -66,7 +66,7 @@ class _TransactionProxy extends Object if (!_hasRolledBack && !_hasFailed) { await execute('COMMIT', timeoutInSeconds: commitTimeoutInSeconds); - completer.complete(result); + _completer.complete(result); } } @@ -100,9 +100,9 @@ class _TransactionProxy extends Object } if (object is _TransactionRollbackException) { - completer.complete(PostgreSQLRollback._(object.reason)); + _completer.complete(PostgreSQLRollback._(object.reason)); } else { - completer.completeError(object, trace); + _completer.completeError(object, trace); } } @@ -131,5 +131,8 @@ class PostgreSQLRollback { PostgreSQLRollback._(this.reason); /// The reason the transaction was cancelled. - String reason; + final String reason; + + @override + String toString() => 'PostgreSQLRollback: $reason'; } diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index a6984eb..306a069 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:mirrors'; +import 'package:postgres/src/query_cache.dart'; import 'package:test/test.dart'; import 'package:postgres/postgres.dart'; @@ -339,7 +340,7 @@ void main() { hasCachedQueryNamed( connection, 'select i1, i2 from t where i1 > @i1'), true); - expect(cachedQueryMap(connection).length, 1); + expect(getQueryCache(connection).length, 1); }); test('Call query multiple times, mixing in other named queries, succeeds', @@ -381,7 +382,7 @@ void main() { expect( hasCachedQueryNamed(connection, 'select i1,i2 from t where i2 < @i2'), true); - expect(cachedQueryMap(connection).length, 2); + expect(getQueryCache(connection).length, 2); }); test( @@ -464,7 +465,7 @@ void main() { expect(true, false); } on PostgreSQLException {} - expect(cachedQueryMap(connection).isEmpty, true); + expect(getQueryCache(connection).isEmpty, true); }); test( @@ -479,7 +480,7 @@ void main() { expect(true, false); } on PostgreSQLException {} - expect(cachedQueryMap(connection).length, 0); + expect(getQueryCache(connection).length, 0); }); test( @@ -555,21 +556,19 @@ void main() { expect(results, [ [1, 2] ]); - expect(cachedQueryMap(connection).length, 1); + expect(getQueryCache(connection).length, 1); expect(hasCachedQueryNamed(connection, string), true); }); }); } -Map cachedQueryMap(PostgreSQLConnection connection) { +QueryCache getQueryCache(PostgreSQLConnection connection) { final cacheMirror = reflect(connection).type.declarations.values.firstWhere( (DeclarationMirror dm) => dm.simpleName.toString().contains('_cache')); - return reflect(connection) - .getField(cacheMirror.simpleName) - .getField(#queries) - .reflectee as Map; + return reflect(connection).getField(cacheMirror.simpleName).reflectee + as QueryCache; } bool hasCachedQueryNamed(PostgreSQLConnection connection, String name) { - return cachedQueryMap(connection)[name] != null; + return getQueryCache(connection)[name] != null; } From 6bde25ee5905e33258499f3d69c5f5014abe539d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sun, 24 Mar 2019 23:41:49 +0100 Subject: [PATCH 47/73] Hardened and more efficient ServerMessages. (#83) * Simpler MessageFramer * Refactor ServerMessage creation. * Hardened and more efficient ServerMessages. * Update ErrorResponseMessage parsing --- lib/src/connection.dart | 2 +- lib/src/message_window.dart | 88 +++++++-------- lib/src/query.dart | 6 +- lib/src/server_messages.dart | 208 ++++++++++++++++------------------- test/framer_test.dart | 86 +++++---------- 5 files changed, 167 insertions(+), 223 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index c67a379..a69d689 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -248,7 +248,7 @@ class PostgreSQLConnection extends Object // anything with that data. _framer.addBytes(castBytes(bytes)); while (_framer.hasMessage) { - final msg = _framer.popMessage().message; + final msg = _framer.popMessage(); try { if (msg is ErrorResponseMessage) { _transitionToState(_connectionState.onErrorResponse(msg)); diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 3c0d193..899f5f5 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -5,45 +5,39 @@ import 'package:buffer/buffer.dart'; import 'server_messages.dart'; -class MessageFrame { - static const int HeaderByteSize = 5; - static Map messageTypeMap = { - 49: () => ParseCompleteMessage(), - 50: () => BindCompleteMessage(), - 65: () => NotificationResponseMessage(), - 67: () => CommandCompleteMessage(), - 68: () => DataRowMessage(), - 69: () => ErrorResponseMessage(), - 75: () => BackendKeyMessage(), - 82: () => AuthenticationMessage(), - 83: () => ParameterStatusMessage(), - 84: () => RowDescriptionMessage(), - 90: () => ReadyForQueryMessage(), - 110: () => NoDataMessage(), - 116: () => ParameterDescriptionMessage() - }; +const int _headerByteSize = 5; +final _emptyData = Uint8List(0); - bool get hasReadHeader => type != null; - int type; - int expectedLength; +typedef ServerMessage _ServerMessageFn(Uint8List data); - bool get isComplete => data != null || expectedLength == 0; - Uint8List data; - - ServerMessage get message { - final msgMaker = - messageTypeMap[type] ?? () => UnknownMessage()..code = type; - - final msg = msgMaker() as ServerMessage; - msg.readBytes(data); - return msg; - } -} +Map _messageTypeMap = { + 49: (d) => ParseCompleteMessage(), + 50: (d) => BindCompleteMessage(), + 65: (d) => NotificationResponseMessage(d), + 67: (d) => CommandCompleteMessage(d), + 68: (d) => DataRowMessage(d), + 69: (d) => ErrorResponseMessage(d), + 75: (d) => BackendKeyMessage(d), + 82: (d) => AuthenticationMessage(d), + 83: (d) => ParameterStatusMessage(d), + 84: (d) => RowDescriptionMessage(d), + 90: (d) => ReadyForQueryMessage(d), + 110: (d) => NoDataMessage(), + 116: (d) => ParameterDescriptionMessage(d), +}; class MessageFramer { final _reader = ByteDataReader(); - MessageFrame messageInProgress = MessageFrame(); - final messageQueue = Queue(); + final messageQueue = Queue(); + + int _type; + int _expectedLength; + + bool get _hasReadHeader => _type != null; + bool get _canReadHeader => _reader.remainingLength >= _headerByteSize; + + bool get _isComplete => + _expectedLength == 0 || _expectedLength <= _reader.remainingLength; void addBytes(Uint8List bytes) { _reader.add(bytes); @@ -51,21 +45,21 @@ class MessageFramer { bool evaluateNextMessage = true; while (evaluateNextMessage) { evaluateNextMessage = false; - if (!messageInProgress.hasReadHeader && - _reader.remainingLength >= MessageFrame.HeaderByteSize) { - messageInProgress.type = _reader.readUint8(); - messageInProgress.expectedLength = _reader.readUint32() - 4; - } - if (messageInProgress.hasReadHeader && - messageInProgress.expectedLength > 0 && - _reader.remainingLength >= messageInProgress.expectedLength) { - messageInProgress.data = _reader.read(messageInProgress.expectedLength); + if (!_hasReadHeader && _canReadHeader) { + _type = _reader.readUint8(); + _expectedLength = _reader.readUint32() - 4; } - if (messageInProgress.isComplete) { - messageQueue.add(messageInProgress); - messageInProgress = MessageFrame(); + if (_hasReadHeader && _isComplete) { + final data = + _expectedLength == 0 ? _emptyData : _reader.read(_expectedLength); + final msgMaker = _messageTypeMap[_type]; + final msg = + msgMaker == null ? UnknownMessage(_type, data) : msgMaker(data); + messageQueue.add(msg); + _type = null; + _expectedLength = null; evaluateNextMessage = true; } } @@ -73,7 +67,7 @@ class MessageFramer { bool get hasMessage => messageQueue.isNotEmpty; - MessageFrame popMessage() { + ServerMessage popMessage() { return messageQueue.removeFirst(); } } diff --git a/lib/src/query.dart b/lib/src/query.dart index 21d49f1..50d42ef 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -129,7 +129,7 @@ class Query { return null; } - void addRow(List rawRowData) { + void addRow(List rawRowData) { if (onlyReturnAffectedRowCount) { return; } @@ -137,9 +137,7 @@ class Query { final iterator = fieldDescriptions.iterator; final lazyDecodedData = rawRowData.map((bd) { iterator.moveNext(); - - return iterator.current.converter - .convert(bd?.buffer?.asUint8List(bd.offsetInBytes, bd.lengthInBytes)); + return iterator.current.converter.convert(bd); }); rows.add(lazyDecodedData.toList()); diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index 2cfbc47..e0d2cbc 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -1,29 +1,38 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:buffer/buffer.dart'; + import 'connection.dart'; import 'query.dart'; -abstract class ServerMessage { - void readBytes(Uint8List bytes); -} +abstract class ServerMessage {} class ErrorResponseMessage implements ServerMessage { - List fields = [ErrorField()]; - - @override - void readBytes(Uint8List bytes) { - final lastByteRemovedList = - Uint8List.view(bytes.buffer, bytes.offsetInBytes, bytes.length - 1); - - lastByteRemovedList.forEach((byte) { - if (byte != 0) { - fields.last.add(byte); - return; + final fields = []; + + ErrorResponseMessage(Uint8List bytes) { + final reader = ByteDataReader()..add(bytes); + + int identificationToken; + StringBuffer sb; + + while (reader.remainingLength > 0) { + final byte = reader.readUint8(); + if (identificationToken == null) { + identificationToken = byte; + sb = StringBuffer(); + } else if (byte == 0) { + fields.add(ErrorField(identificationToken, sb.toString())); + identificationToken = null; + sb = null; + } else { + sb.writeCharCode(byte); } - - fields.add(ErrorField()); - }); + } + if (identificationToken != null && sb != null) { + fields.add(ErrorField(identificationToken, sb.toString())); + } } } @@ -37,33 +46,33 @@ class AuthenticationMessage implements ServerMessage { static const int KindGSSContinue = 8; static const int KindSSPI = 9; - int type; - - List salt; + final int type; + final List salt; - @override - void readBytes(Uint8List bytes) { - final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); - type = view.getUint32(0); + AuthenticationMessage._(this.type, this.salt); + factory AuthenticationMessage(Uint8List bytes) { + final reader = ByteDataReader()..add(bytes); + final type = reader.readUint32(); + List salt; if (type == KindMD5Password) { - salt = List(4); - for (var i = 0; i < 4; i++) { - salt[i] = view.getUint8(4 + i); - } + salt = reader.read(4, copy: true); } + return AuthenticationMessage._(type, salt); } } class ParameterStatusMessage extends ServerMessage { - String name; - String value; + final String name; + final String value; - @override - void readBytes(Uint8List bytes) { - name = utf8.decode(bytes.sublist(0, bytes.indexOf(0))); - value = - utf8.decode(bytes.sublist(bytes.indexOf(0) + 1, bytes.lastIndexOf(0))); + ParameterStatusMessage._(this.name, this.value); + + factory ParameterStatusMessage(Uint8List bytes) { + final first0 = bytes.indexOf(0); + final name = utf8.decode(bytes.sublist(0, first0)); + final value = utf8.decode(bytes.sublist(first0 + 1, bytes.lastIndexOf(0))); + return ParameterStatusMessage._(name, value); } } @@ -72,37 +81,34 @@ class ReadyForQueryMessage extends ServerMessage { static const String StateTransaction = 'T'; static const String StateTransactionError = 'E'; - String state; + final String state; - @override - void readBytes(Uint8List bytes) { - state = utf8.decode(bytes); - } + ReadyForQueryMessage(Uint8List bytes) : state = utf8.decode(bytes); } class BackendKeyMessage extends ServerMessage { - int processID; - int secretKey; + final int processID; + final int secretKey; - @override - void readBytes(Uint8List bytes) { + BackendKeyMessage._(this.processID, this.secretKey); + + factory BackendKeyMessage(Uint8List bytes) { final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); - processID = view.getUint32(0); - secretKey = view.getUint32(4); + final processID = view.getUint32(0); + final secretKey = view.getUint32(4); + return BackendKeyMessage._(processID, secretKey); } } class RowDescriptionMessage extends ServerMessage { - List fieldDescriptions; + final fieldDescriptions = []; - @override - void readBytes(Uint8List bytes) { + RowDescriptionMessage(Uint8List bytes) { final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); int offset = 0; final fieldCount = view.getInt16(offset); offset += 2; - fieldDescriptions = []; for (var i = 0; i < fieldCount; i++) { final rowDesc = FieldDescription(); offset = rowDesc.parse(view, offset); @@ -112,28 +118,22 @@ class RowDescriptionMessage extends ServerMessage { } class DataRowMessage extends ServerMessage { - List values = []; + final values = []; - @override - void readBytes(Uint8List bytes) { - final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); - int offset = 0; - final fieldCount = view.getInt16(offset); - offset += 2; + DataRowMessage(Uint8List bytes) { + final reader = ByteDataReader()..add(bytes); + final fieldCount = reader.readInt16(); for (var i = 0; i < fieldCount; i++) { - final dataSize = view.getInt32(offset); - offset += 4; + final dataSize = reader.readInt32(); if (dataSize == 0) { - values.add(ByteData(0)); + values.add(Uint8List(0)); } else if (dataSize == -1) { values.add(null); } else { - final rawBytes = - ByteData.view(bytes.buffer, bytes.offsetInBytes + offset, dataSize); + final rawBytes = reader.read(dataSize); values.add(rawBytes); - offset += dataSize; } } } @@ -143,90 +143,80 @@ class DataRowMessage extends ServerMessage { } class NotificationResponseMessage extends ServerMessage { - int processID; - String channel; - String payload; + final int processID; + final String channel; + final String payload; - @override - void readBytes(Uint8List bytes) { + NotificationResponseMessage._(this.processID, this.channel, this.payload); + + factory NotificationResponseMessage(Uint8List bytes) { final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); - processID = view.getUint32(0); - channel = utf8.decode(bytes.sublist(4, bytes.indexOf(0, 4))); - payload = utf8 - .decode(bytes.sublist(bytes.indexOf(0, 4) + 1, bytes.lastIndexOf(0))); + final processID = view.getUint32(0); + final first0 = bytes.indexOf(0, 4); + final channel = utf8.decode(bytes.sublist(4, first0)); + final payload = + utf8.decode(bytes.sublist(first0 + 1, bytes.lastIndexOf(0))); + return NotificationResponseMessage._(processID, channel, payload); } } class CommandCompleteMessage extends ServerMessage { - int rowsAffected; + final int rowsAffected; static RegExp identifierExpression = RegExp(r'[A-Z ]*'); - @override - void readBytes(Uint8List bytes) { - final str = utf8.decode(bytes.sublist(0, bytes.length - 1)); + CommandCompleteMessage._(this.rowsAffected); + factory CommandCompleteMessage(Uint8List bytes) { + final str = utf8.decode(bytes.sublist(0, bytes.length - 1)); final match = identifierExpression.firstMatch(str); + int rowsAffected = 0; if (match.end < str.length) { rowsAffected = int.parse(str.split(' ').last); - } else { - rowsAffected = 0; } + return CommandCompleteMessage._(rowsAffected); } } class ParseCompleteMessage extends ServerMessage { - @override - void readBytes(Uint8List bytes) {} + ParseCompleteMessage(); @override String toString() => 'Parse Complete Message'; } class BindCompleteMessage extends ServerMessage { - @override - void readBytes(Uint8List bytes) {} + BindCompleteMessage(); @override String toString() => 'Bind Complete Message'; } class ParameterDescriptionMessage extends ServerMessage { - List parameterTypeIDs; - - @override - void readBytes(Uint8List bytes) { - final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); + final parameterTypeIDs = []; - int offset = 0; - final count = view.getUint16(0); - offset += 2; + ParameterDescriptionMessage(Uint8List bytes) { + final reader = ByteDataReader()..add(bytes); + final count = reader.readUint16(); - parameterTypeIDs = []; for (var i = 0; i < count; i++) { - final v = view.getUint32(offset); - offset += 4; - parameterTypeIDs.add(v); + parameterTypeIDs.add(reader.readUint32()); } } } class NoDataMessage extends ServerMessage { - @override - void readBytes(Uint8List bytes) {} + NoDataMessage(); @override String toString() => 'No Data Message'; } class UnknownMessage extends ServerMessage { - Uint8List bytes; - int code; + final int code; + final Uint8List bytes; - @override - void readBytes(Uint8List bytes) { - this.bytes = bytes; - } + UnknownMessage(this.code, this.bytes); @override int get hashCode { @@ -295,16 +285,8 @@ class ErrorField { return PostgreSQLSeverity.unknown; } - int identificationToken; - - String get text => _buffer.toString(); - final _buffer = StringBuffer(); + final int identificationToken; + final String text; - void add(int byte) { - if (identificationToken == null) { - identificationToken = byte; - } else { - _buffer.writeCharCode(byte); - } - } + ErrorField(this.identificationToken, this.text); } diff --git a/test/framer_test.dart b/test/framer_test.dart index 25f62d7..79139e3 100644 --- a/test/framer_test.dart +++ b/test/framer_test.dart @@ -22,11 +22,9 @@ void main() { messageWithBytes([1, 2, 3], 1) ])); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]) + UnknownMessage(1, Uint8List.fromList([1, 2, 3])), ]); }); @@ -36,14 +34,10 @@ void main() { messageWithBytes([1, 2, 3, 4], 2) ])); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]), - UnknownMessage() - ..code = 2 - ..bytes = Uint8List.fromList([1, 2, 3, 4]) + UnknownMessage(1, Uint8List.fromList([1, 2, 3])), + UnknownMessage(2, Uint8List.fromList([1, 2, 3, 4])), ]); }); @@ -55,11 +49,9 @@ void main() { framer.addBytes(fragments.last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]) + UnknownMessage(1, Uint8List.fromList([1, 2, 3])) ]); }); @@ -76,11 +68,9 @@ void main() { framer.addBytes(fragments.last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]) + UnknownMessage(1, Uint8List.fromList([1, 2, 3])), ]); }); @@ -95,14 +85,10 @@ void main() { framer.addBytes(message2Fragments.last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]), - UnknownMessage() - ..code = 2 - ..bytes = Uint8List.fromList([2, 2, 3]), + UnknownMessage(1, Uint8List.fromList([1, 2, 3])), + UnknownMessage(2, Uint8List.fromList([2, 2, 3])), ]); }); @@ -117,14 +103,10 @@ void main() { framer.addBytes(message2Fragments.last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]), - UnknownMessage() - ..code = 2 - ..bytes = Uint8List.fromList([2, 2, 3]), + UnknownMessage(1, Uint8List.fromList([1, 2, 3])), + UnknownMessage(2, Uint8List.fromList([2, 2, 3])), ]); }); @@ -136,11 +118,9 @@ void main() { framer.addBytes(fragments.last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) + UnknownMessage(1, Uint8List.fromList([1, 2, 3, 4, 5, 6, 7])), ]); }); @@ -156,14 +136,10 @@ void main() { framer.addBytes(fragmentedMessageBuffer(message, 8).last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 0 - ..bytes = Uint8List.fromList([1, 2]), - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7]) + UnknownMessage(0, Uint8List.fromList([1, 2])), + UnknownMessage(1, Uint8List.fromList([1, 2, 3, 4, 5, 6, 7])) ]); }); @@ -185,23 +161,19 @@ void main() { fragmentedMessageBuffer(fragmentedMessageBuffer(message, 3).last, 6) .last); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 0 - ..bytes = Uint8List.fromList([1, 2]), - UnknownMessage() - ..code = 1 - ..bytes = - Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + UnknownMessage(0, Uint8List.fromList([1, 2])), + UnknownMessage( + 1, Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])), ]); }); test('Frame with no data', () { framer.addBytes(bufferWithMessages([messageWithBytes([], 10)])); - final messages = framer.messageQueue.map((f) => f.message).toList(); - expect(messages, [UnknownMessage()..code = 10]); + final messages = framer.messageQueue.toList(); + expect(messages, [UnknownMessage(10, Uint8List(0))]); }); } @@ -231,10 +203,8 @@ flush(MessageFramer framer) { messageWithBytes([1, 2, 3], 1) ])); - final messages = framer.messageQueue.map((f) => f.message).toList(); + final messages = framer.messageQueue.toList(); expect(messages, [ - UnknownMessage() - ..code = 1 - ..bytes = Uint8List.fromList([1, 2, 3]) + UnknownMessage(1, Uint8List.fromList([1, 2, 3])), ]); } From d8ea01cd6f738f353a96881b888ca68150dd383c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 25 Mar 2019 00:07:40 +0100 Subject: [PATCH 48/73] More efficient binary codec. (#84) --- lib/src/binary_codec.dart | 83 +++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index 541854c..89c75ce 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -6,6 +6,28 @@ import 'package:buffer/buffer.dart'; import '../postgres.dart'; import 'types.dart'; +final _bool0 = Uint8List(1)..[0] = 0; +final _bool1 = Uint8List(1)..[0] = 1; +final _dashUnit = '-'.codeUnits.first; +final _hex = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', +]; + class PostgresBinaryEncoder extends Converter { final PostgreSQLDataType _dataType; @@ -21,9 +43,7 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.boolean: { if (value is bool) { - final bd = ByteData(1); - bd.setUint8(0, value ? 1 : 0); - return bd.buffer.asUint8List(); + return value ? _bool1 : _bool0; } throw FormatException( 'Invalid type for parameter value. Expected: bool Got: ${value.runtimeType}'); @@ -127,13 +147,10 @@ class PostgresBinaryEncoder extends Converter { case PostgreSQLDataType.json: { final jsonBytes = utf8.encode(json.encode(value)); - final outBuffer = Uint8List(jsonBytes.length + 1); - outBuffer[0] = 1; - for (var i = 0; i < jsonBytes.length; i++) { - outBuffer[i + 1] = jsonBytes[i]; - } - - return outBuffer; + final writer = ByteDataWriter(bufferLength: jsonBytes.length + 1); + writer.writeUint8(1); + writer.write(jsonBytes); + return writer.toBytes(); } case PostgreSQLDataType.byteArray: @@ -152,11 +169,10 @@ class PostgresBinaryEncoder extends Converter { 'Invalid type for parameter value. Expected: String Got: ${value.runtimeType}'); } - final dashUnit = '-'.codeUnits.first; final hexBytes = (value as String) .toLowerCase() .codeUnits - .where((c) => c != dashUnit) + .where((c) => c != _dashUnit) .toList(); if (hexBytes.length != 32) { throw FormatException( @@ -175,11 +191,11 @@ class PostgresBinaryEncoder extends Converter { }; final outBuffer = Uint8List(16); - for (var i = 0; i < hexBytes.length; i += 2) { + for (var i = 0, j = 0; i < hexBytes.length; i += 2, j++) { final upperByte = byteConvert(hexBytes[i]); final lowerByte = byteConvert(hexBytes[i + 1]); - outBuffer[i ~/ 2] = upperByte * 16 + lowerByte; + outBuffer[j] = (upperByte << 4) + lowerByte; } return outBuffer; } @@ -208,8 +224,7 @@ class PostgresBinaryDecoder extends Converter { switch (dataType) { case PostgreSQLDataType.name: case PostgreSQLDataType.text: - return utf8.decode( - value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes)); + return utf8.decode(value); case PostgreSQLDataType.boolean: return buffer.getInt8(0) != 0; case PostgreSQLDataType.smallInteger: @@ -241,46 +256,22 @@ class PostgresBinaryDecoder extends Converter { } case PostgreSQLDataType.byteArray: - return value.buffer - .asUint8List(value.offsetInBytes, value.lengthInBytes); + return value; case PostgreSQLDataType.uuid: { - final codeDash = '-'.codeUnitAt(0); - - final cipher = [ - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f' - ]; - final byteConvert = (int value) { - return cipher[value]; - }; - final buf = StringBuffer(); for (var i = 0; i < buffer.lengthInBytes; i++) { final byteValue = buffer.getUint8(i); - final upperByteValue = byteValue ~/ 16; + final upperByteValue = byteValue >> 4; + final lowerByteValue = byteValue & 0x0f; - final upperByteHex = byteConvert(upperByteValue); - final lowerByteHex = byteConvert(byteValue - (upperByteValue * 16)); + final upperByteHex = _hex[upperByteValue]; + final lowerByteHex = _hex[lowerByteValue]; buf.write(upperByteHex); buf.write(lowerByteHex); if (i == 3 || i == 5 || i == 7 || i == 9) { - buf.writeCharCode(codeDash); + buf.writeCharCode(_dashUnit); } } From 14503ce9e2ec16658323ddade96531250a7bb17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Tue, 26 Mar 2019 21:08:49 +0100 Subject: [PATCH 49/73] Simplified ClientMessages. (#85) --- lib/src/client_messages.dart | 102 +++++++++++------------------------ 1 file changed, 32 insertions(+), 70 deletions(-) diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index f737953..03910c5 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -21,17 +21,6 @@ abstract class ClientMessage { static const int SyncIdentifier = 83; static const int PasswordIdentifier = 112; - int get length; - - void applyStringToBuffer(UTF8BackedString string, ByteDataWriter buffer) { - buffer.write(string.utf8Bytes); - buffer.writeInt8(0); - } - - void applyBytesToBuffer(List bytes, ByteDataWriter buffer) { - buffer.write(bytes); - } - void applyToBuffer(ByteDataWriter buffer); Uint8List asBytes() { @@ -47,6 +36,11 @@ abstract class ClientMessage { } } +void _applyStringToBuffer(UTF8BackedString string, ByteDataWriter buffer) { + buffer.write(string.utf8Bytes); + buffer.writeInt8(0); +} + class StartupMessage extends ClientMessage { final UTF8BackedString _username; final UTF8BackedString _databaseName; @@ -58,34 +52,29 @@ class StartupMessage extends ClientMessage { _username = username == null ? null : UTF8BackedString(username); @override - int get length { + void applyToBuffer(ByteDataWriter buffer) { final fixedLength = 53; final variableLength = (_username?.utf8Length ?? 0) + _databaseName.utf8Length + _timeZone.utf8Length + 3; - return fixedLength + variableLength; - } - - @override - void applyToBuffer(ByteDataWriter buffer) { - buffer.writeInt32(length); + buffer.writeInt32(fixedLength + variableLength); buffer.writeInt32(ClientMessage.ProtocolVersion); if (_username != null) { - applyBytesToBuffer((UTF8ByteConstants.user), buffer); - applyStringToBuffer(_username, buffer); + buffer.write(UTF8ByteConstants.user); + _applyStringToBuffer(_username, buffer); } - applyBytesToBuffer(UTF8ByteConstants.database, buffer); - applyStringToBuffer(_databaseName, buffer); + buffer.write(UTF8ByteConstants.database); + _applyStringToBuffer(_databaseName, buffer); - applyBytesToBuffer(UTF8ByteConstants.clientEncoding, buffer); - applyBytesToBuffer(UTF8ByteConstants.utf8, buffer); + buffer.write(UTF8ByteConstants.clientEncoding); + buffer.write(UTF8ByteConstants.utf8); - applyBytesToBuffer(UTF8ByteConstants.timeZone, buffer); - applyStringToBuffer(_timeZone, buffer); + buffer.write(UTF8ByteConstants.timeZone); + _applyStringToBuffer(_timeZone, buffer); buffer.writeInt8(0); } @@ -102,16 +91,12 @@ class AuthMD5Message extends ClientMessage { _hashedAuthString = UTF8BackedString('md5$md5Hash'); } - @override - int get length { - return 6 + _hashedAuthString.utf8Length; - } - @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.PasswordIdentifier); - buffer.writeUint32(length - 1); - applyStringToBuffer(_hashedAuthString, buffer); + final length = 5 + _hashedAuthString.utf8Length; + buffer.writeUint32(length); + _applyStringToBuffer(_hashedAuthString, buffer); } } @@ -121,16 +106,12 @@ class QueryMessage extends ClientMessage { QueryMessage(String queryString) : _queryString = UTF8BackedString(queryString); - @override - int get length { - return 6 + _queryString.utf8Length; - } - @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.QueryIdentifier); - buffer.writeUint32(length - 1); - applyStringToBuffer(_queryString, buffer); + final length = 5 + _queryString.utf8Length; + buffer.writeUint32(length); + _applyStringToBuffer(_queryString, buffer); } } @@ -142,18 +123,14 @@ class ParseMessage extends ClientMessage { : _statement = UTF8BackedString(statement), _statementName = UTF8BackedString(statementName); - @override - int get length { - return 9 + _statement.utf8Length + _statementName.utf8Length; - } - @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.ParseIdentifier); - buffer.writeUint32(length - 1); + final length = 8 + _statement.utf8Length + _statementName.utf8Length; + buffer.writeUint32(length); // Name of prepared statement - applyStringToBuffer(_statementName, buffer); - applyStringToBuffer(_statement, buffer); // Query string + _applyStringToBuffer(_statementName, buffer); + _applyStringToBuffer(_statement, buffer); // Query string buffer.writeUint16(0); } } @@ -164,17 +141,13 @@ class DescribeMessage extends ClientMessage { DescribeMessage({String statementName = ''}) : _statementName = UTF8BackedString(statementName); - @override - int get length { - return 7 + _statementName.utf8Length; - } - @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.DescribeIdentifier); - buffer.writeUint32(length - 1); + final length = 6 + _statementName.utf8Length; + buffer.writeUint32(length); buffer.writeUint8(83); - applyStringToBuffer(_statementName, buffer); // Name of prepared statement + _applyStringToBuffer(_statementName, buffer); // Name of prepared statement } } @@ -188,7 +161,6 @@ class BindMessage extends ClientMessage { : _typeSpecCount = _parameters.where((p) => p.isBinary).length, _statementName = UTF8BackedString(statementName); - @override int get length { if (_cachedLength == null) { var inputParameterElementCount = _parameters.length; @@ -217,9 +189,9 @@ class BindMessage extends ClientMessage { buffer.writeUint32(length - 1); // Name of portal - currently unnamed portal. - applyBytesToBuffer([0], buffer); + buffer.writeUint8(0); // Name of prepared statement. - applyStringToBuffer(_statementName, buffer); + _applyStringToBuffer(_statementName, buffer); // OK, if we have no specified types at all, we can use 0. If we have all specified types, we can use 1. If we have a mix, we have to individually // call out each type. @@ -258,26 +230,16 @@ class BindMessage extends ClientMessage { } class ExecuteMessage extends ClientMessage { - @override - int get length { - return 10; - } - @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.ExecuteIdentifier); - buffer.writeUint32(length - 1); - applyBytesToBuffer([0], buffer); // Portal name + buffer.writeUint32(9); + buffer.writeUint8(0); // Portal name buffer.writeUint32(0); } } class SyncMessage extends ClientMessage { - @override - int get length { - return 5; - } - @override void applyToBuffer(ByteDataWriter buffer) { buffer.writeUint8(ClientMessage.SyncIdentifier); From 381bd1bd5a226a2fbaaf87702d74079bf4c8fd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Wed, 27 Mar 2019 22:21:59 +0100 Subject: [PATCH 50/73] Harden a few things in query.dart (#86) --- lib/src/connection.dart | 6 +- lib/src/query.dart | 109 +++++++++++++++++++-------------- lib/src/server_messages.dart | 9 +-- lib/src/transaction_proxy.dart | 8 +-- 4 files changed, 74 insertions(+), 58 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index a69d689..ee05d5b 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -388,9 +388,9 @@ abstract class _PostgreSQLExecutionContextMixin 'Attempting to execute query, but connection is not open.'); } - final query = - Query(fmtString, substitutionValues, _connection, _transaction) - ..onlyReturnAffectedRowCount = true; + final query = Query( + fmtString, substitutionValues, _connection, _transaction, + onlyReturnAffectedRowCount: true); return _enqueue(query, timeoutInSeconds: timeoutInSeconds); } diff --git a/lib/src/query.dart b/lib/src/query.dart index 50d42ef..2b9c812 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -14,10 +14,15 @@ import 'text_codec.dart'; import 'types.dart'; class Query { - Query(this.statement, this.substitutionValues, this.connection, - this.transaction); + Query( + this.statement, + this.substitutionValues, + this.connection, + this.transaction, { + this.onlyReturnAffectedRowCount = false, + }); - bool onlyReturnAffectedRowCount = false; + final bool onlyReturnAffectedRowCount; String statementIdentifier; @@ -171,8 +176,8 @@ class Query { class CachedQuery { CachedQuery(this.preparedStatementName, this.orderedParameters); - String preparedStatementName; - List orderedParameters; + final String preparedStatementName; + final List orderedParameters; List fieldDescriptions; bool get isValid { @@ -193,69 +198,76 @@ class ParameterValue { substitutionValues[identifier.name], identifier.type); } - ParameterValue.binary(dynamic value, PostgreSQLDataType postgresType) - : isBinary = true { + factory ParameterValue.binary( + dynamic value, PostgreSQLDataType postgresType) { final converter = PostgresBinaryEncoder(postgresType); - bytes = converter.convert(value); - length = bytes?.length ?? 0; + final bytes = converter.convert(value); + final length = bytes?.length ?? 0; + return ParameterValue._(true, bytes, length); } - ParameterValue.text(dynamic value) : isBinary = false { + factory ParameterValue.text(dynamic value) { + Uint8List bytes; if (value != null) { final converter = PostgresTextEncoder(false); bytes = castBytes(utf8.encode(converter.convert(value))); } - length = bytes?.length; + final length = bytes?.length ?? 0; + return ParameterValue._(false, bytes, length); } + ParameterValue._(this.isBinary, this.bytes, this.length); + final bool isBinary; - Uint8List bytes; - int length; + final Uint8List bytes; + final int length; } class FieldDescription { - Converter converter; + final Converter converter; - String fieldName; - int tableID; - int columnID; - int typeID; - int dataTypeSize; - int typeModifier; - int formatCode; + final String fieldName; + final int tableID; + final int columnID; + final int typeID; + final int dataTypeSize; + final int typeModifier; + final int formatCode; String resolvedTableName; - int parse(ByteData byteData, int initialOffset) { - int offset = initialOffset; + FieldDescription._( + this.converter, + this.fieldName, + this.tableID, + this.columnID, + this.typeID, + this.dataTypeSize, + this.typeModifier, + this.formatCode); + + factory FieldDescription.read(ByteDataReader reader) { final buf = StringBuffer(); int byte = 0; do { - byte = byteData.getUint8(offset); - offset += 1; + byte = reader.readUint8(); if (byte != 0) { buf.writeCharCode(byte); } } while (byte != 0); - fieldName = buf.toString(); - - tableID = byteData.getUint32(offset); - offset += 4; - columnID = byteData.getUint16(offset); - offset += 2; - typeID = byteData.getUint32(offset); - offset += 4; - dataTypeSize = byteData.getUint16(offset); - offset += 2; - typeModifier = byteData.getInt32(offset); - offset += 4; - formatCode = byteData.getUint16(offset); - offset += 2; + final fieldName = buf.toString(); - converter = PostgresBinaryDecoder(typeID); + final tableID = reader.readUint32(); + final columnID = reader.readUint16(); + final typeID = reader.readUint32(); + final dataTypeSize = reader.readUint16(); + final typeModifier = reader.readInt32(); + final formatCode = reader.readUint16(); - return offset; + final converter = PostgresBinaryDecoder(typeID); + return FieldDescription._(converter, fieldName, tableID, columnID, typeID, + dataTypeSize, typeModifier, formatCode); } @override @@ -294,7 +306,11 @@ class PostgreSQLFormatIdentifier { 'uuid': PostgreSQLDataType.uuid }; - PostgreSQLFormatIdentifier(String t) { + factory PostgreSQLFormatIdentifier(String t) { + String name; + PostgreSQLDataType type; + String typeCast; + final components = t.split('::'); if (components.length > 1) { typeCast = components.sublist(1).join(''); @@ -321,9 +337,12 @@ class PostgreSQLFormatIdentifier { // Strip @ name = name.substring(1, name.length); + return PostgreSQLFormatIdentifier._(name, type, typeCast); } - String name; - PostgreSQLDataType type; - String typeCast; + PostgreSQLFormatIdentifier._(this.name, this.type, this.typeCast); + + final String name; + final PostgreSQLDataType type; + final String typeCast; } diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index e0d2cbc..9d48d49 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -104,14 +104,11 @@ class RowDescriptionMessage extends ServerMessage { final fieldDescriptions = []; RowDescriptionMessage(Uint8List bytes) { - final view = ByteData.view(bytes.buffer, bytes.offsetInBytes); - int offset = 0; - final fieldCount = view.getInt16(offset); - offset += 2; + final reader = ByteDataReader()..add(bytes); + final fieldCount = reader.readInt16(); for (var i = 0; i < fieldCount; i++) { - final rowDesc = FieldDescription(); - offset = rowDesc.parse(view, offset); + final rowDesc = FieldDescription.read(reader); fieldDescriptions.add(rowDesc); } } diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index eff2e8d..294266d 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -8,8 +8,8 @@ class _TransactionProxy extends Object implements PostgreSQLExecutionContext { _TransactionProxy( this._connection, this.executionBlock, this.commitTimeoutInSeconds) { - _beginQuery = Query('BEGIN', {}, _connection, this) - ..onlyReturnAffectedRowCount = true; + _beginQuery = Query('BEGIN', {}, _connection, this, + onlyReturnAffectedRowCount: true); _beginQuery.future.then(startTransaction).catchError((err, StackTrace st) { Future(() { @@ -87,8 +87,8 @@ class _TransactionProxy extends Object 'that prevented this query from executing.'); _queue.cancel(err); - final rollback = Query('ROLLBACK', {}, _connection, _transaction) - ..onlyReturnAffectedRowCount = true; + final rollback = Query('ROLLBACK', {}, _connection, _transaction, + onlyReturnAffectedRowCount: true); _queue.addEvenIfCancelled(rollback); _connection._transitionToState(_connection._connectionState.awake()); From 753908d14f62ef1b5a0c78e4f1cae4d51fe42773 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 2 Apr 2019 19:48:51 +0200 Subject: [PATCH 51/73] Bump version --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f968d..0347961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ # Changelog -## 1.0.3 +## 2.0.0-dev1 - Restricted field access on [PostgreSQLConnection]. - Connection-level default query timeout. - Option to specify timeout for the transaction's `"COMMIT"` query. +- Optimized byte buffer parsing and construction with `package:buffer`. +- Hardened codebase with `package:pedantic` and additional lints. +- Updated codebase to Dart 2.2. ## 1.0.2 - Add connection queue size diff --git a/pubspec.yaml b/pubspec.yaml index 3d853df..63ddf08 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 1.0.2 +version: 2.0.0-dev1.0 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: From 97c39901d1c604197475fe1429292cb48a94ba66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 25 Apr 2019 20:37:29 +0200 Subject: [PATCH 52/73] PostgreSQLResult (#90) * Bump version * PostgreSQLResult --- CHANGELOG.md | 2 ++ lib/src/connection.dart | 17 +++++++++++++++-- lib/src/execution_context.dart | 12 +++++++++++- test/map_return_test.dart | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0347961..90d4e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Optimized byte buffer parsing and construction with `package:buffer`. - Hardened codebase with `package:pedantic` and additional lints. - Updated codebase to Dart 2.2. +- `PostgreSQLResult` and `PostgreSQLResultRow` as the return value of a query. + - Returned lists are protected with `UnmodifiableListView`. ## 1.0.2 - Add connection queue size diff --git a/lib/src/connection.dart b/lib/src/connection.dart index ee05d5b..3653898 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -1,6 +1,7 @@ library postgres.connection; import 'dart:async'; +import 'dart:collection'; import 'dart:io'; import 'dart:typed_data'; @@ -337,7 +338,7 @@ abstract class _PostgreSQLExecutionContextMixin int get queueSize => _queue.length; @override - Future>> query(String fmtString, + Future query(String fmtString, {Map substitutionValues, bool allowReuse = true, int timeoutInSeconds}) async { @@ -353,7 +354,9 @@ abstract class _PostgreSQLExecutionContextMixin query.statementIdentifier = _connection._cache.identifierForQuery(query); } - return _enqueue(query, timeoutInSeconds: timeoutInSeconds); + final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); + return _PostgreSQLResult( + rows.map((columns) => _PostgreSQLResultRow(columns)).toList()); } @override @@ -474,3 +477,13 @@ abstract class _PostgreSQLExecutionContextMixin Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async {} } + +class _PostgreSQLResult extends UnmodifiableListView + implements PostgreSQLResult { + _PostgreSQLResult(List rows) : super(rows); +} + +class _PostgreSQLResultRow extends UnmodifiableListView + implements PostgreSQLResultRow { + _PostgreSQLResultRow(List columns) : super(columns); +} diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index e526625..18cdbd8 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -29,7 +29,7 @@ abstract class PostgreSQLExecutionContext { /// By default, instances of this class will reuse queries. This allows significantly more efficient transport to and from the database. You do not have to do /// anything to opt in to this behavior, this connection will track the necessary information required to reuse queries without intervention. (The [fmtString] is /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. - Future>> query(String fmtString, + Future query(String fmtString, {Map substitutionValues, bool allowReuse = true, int timeoutInSeconds}); @@ -86,3 +86,13 @@ abstract class PostgreSQLExecutionContext { bool allowReuse = true, int timeoutInSeconds}); } + +/// A single row of a query result. +/// +/// Column values can be accessed through the [] [List] accessor. +abstract class PostgreSQLResultRow implements List {} + +/// The query result. +/// +/// Rows can be accessed through the [] [List] accessor. +abstract class PostgreSQLResult implements List {} diff --git a/test/map_return_test.dart b/test/map_return_test.dart index bdcb463..f38d89d 100644 --- a/test/map_return_test.dart +++ b/test/map_return_test.dart @@ -146,7 +146,7 @@ class InterceptingConnection extends PostgreSQLConnection { List queries = []; @override - Future>> query(String fmtString, + Future query(String fmtString, {Map substitutionValues, bool allowReuse = true, int timeoutInSeconds}) { From 05f30de1ac3112779c05d3ee1f2ee2fcce80916f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 25 Apr 2019 22:27:45 +0200 Subject: [PATCH 53/73] PostgreSQLConnection and _TransactionProxy share the OID cache. (#91) --- CHANGELOG.md | 1 + lib/src/connection.dart | 80 +++++++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d4e20..25598e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Updated codebase to Dart 2.2. - `PostgreSQLResult` and `PostgreSQLResultRow` as the return value of a query. - Returned lists are protected with `UnmodifiableListView`. +- `PostgreSQLConnection` and `_TransactionProxy` share the OID cache. ## 1.0.2 - Add connection queue size diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 3653898..f344782 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -104,6 +104,7 @@ class PostgreSQLConnection extends Object final Map settings = {}; final _cache = QueryCache(); + final _oidCache = _OidCache(); Socket _socket; MessageFramer _framer = MessageFramer(); int _processID; @@ -325,9 +326,44 @@ class Notification { final String payload; } +class _OidCache { + final _tableOIDNameMap = {}; + + Future _resolveOids( + PostgreSQLExecutionContext c, List columns) async { + //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. + // It's not a significant impact here, but an area for optimization. This includes + // assigning resolvedTableName + final unresolvedTableOIDs = columns + .map((f) => f.tableID) + .toSet() + .where((oid) => + oid != null && oid > 0 && !_tableOIDNameMap.containsKey(oid)) + .toList() + ..sort(); + + if (unresolvedTableOIDs.isNotEmpty) { + await _resolveTableOIDs(c, unresolvedTableOIDs); + } + } + + Future _resolveTableOIDs(PostgreSQLExecutionContext c, List oids) async { + final unresolvedIDString = oids.join(','); + final orderedTableNames = await c.query( + "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + + final iterator = oids.iterator; + orderedTableNames.forEach((tableName) { + iterator.moveNext(); + if (tableName.first != null) { + _tableOIDNameMap[iterator.current] = tableName.first as String; + } + }); + } +} + abstract class _PostgreSQLExecutionContextMixin implements PostgreSQLExecutionContext { - final _tableOIDNameMap = {}; final _queue = QueryQueue(); PostgreSQLConnection get _connection; @@ -378,8 +414,9 @@ abstract class _PostgreSQLExecutionContextMixin } final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); - - return _mapifyRows(rows, query.fieldDescriptions); + final columns = query.fieldDescriptions; + await _connection._oidCache._resolveOids(this, columns); + return _mapifyRows(rows, columns); } @override @@ -401,27 +438,14 @@ abstract class _PostgreSQLExecutionContextMixin @override void cancelTransaction({String reason}); - Future>>> _mapifyRows( - List> rows, List columns) async { - //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. - // It's not a significant impact here, but an area for optimization. This includes - // assigning resolvedTableName - final tableOIDs = Set.from(columns.map((f) => f.tableID)); - final List unresolvedTableOIDs = tableOIDs - .where((oid) => - oid != null && oid > 0 && !_tableOIDNameMap.containsKey(oid)) - .toList(); - unresolvedTableOIDs.sort((int lhs, int rhs) => lhs.compareTo(rhs)); - - if (unresolvedTableOIDs.isNotEmpty) { - await _resolveTableOIDs(unresolvedTableOIDs); - } - + List>> _mapifyRows( + List> rows, List columns) { columns.forEach((desc) { - desc.resolvedTableName = _tableOIDNameMap[desc.tableID]; + desc.resolvedTableName = + _connection._oidCache._tableOIDNameMap[desc.tableID]; }); - final tableNames = tableOIDs.map((oid) => _tableOIDNameMap[oid]).toList(); + final tableNames = columns.map((c) => c.resolvedTableName).toSet().toList(); return rows.map((row) { final rowMap = Map>.fromIterable(tableNames, key: (name) => name as String, value: (_) => {}); @@ -437,20 +461,6 @@ abstract class _PostgreSQLExecutionContextMixin }).toList(); } - Future _resolveTableOIDs(List oids) async { - final unresolvedIDString = oids.join(','); - final orderedTableNames = await query( - "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); - - final iterator = oids.iterator; - orderedTableNames.forEach((tableName) { - iterator.moveNext(); - if (tableName.first != null) { - _tableOIDNameMap[iterator.current] = tableName.first as String; - } - }); - } - Future _enqueue(Query query, {int timeoutInSeconds = 30}) async { if (_queue.add(query)) { _connection._transitionToState(_connection._connectionState.awake()); From ab6e87789477ec0d6fecc9bde4f518c00063374f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sun, 26 May 2019 01:28:51 +0200 Subject: [PATCH 54/73] Fix pedantic lints. (#95) --- test/connection_test.dart | 20 +++++++++++++++----- test/query_reuse_test.dart | 16 ++++++++++++---- test/timeout_test.dart | 16 ++++++++++++---- test/transaction_test.dart | 20 +++++++++++++++----- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/test/connection_test.dart b/test/connection_test.dart index b1c2b52..d95b7d0 100644 --- a/test/connection_test.dart +++ b/test/connection_test.dart @@ -465,7 +465,9 @@ void main() { try { await conn.open(); expect(true, false); - } on SocketException {} + } on SocketException { + // ignore + } await expectConnectionIsInvalid(conn); }); @@ -479,7 +481,9 @@ void main() { try { await conn.open(); expect(true, false); - } on SocketException {} + } on SocketException { + // ignore + } await expectConnectionIsInvalid(conn); }); @@ -501,7 +505,9 @@ void main() { try { await conn.open(); fail('unreachable'); - } on TimeoutException {} + } on TimeoutException { + // ignore + } await expectConnectionIsInvalid(conn); }); @@ -523,7 +529,9 @@ void main() { try { await conn.open(); fail('unreachable'); - } on TimeoutException {} + } on TimeoutException { + // ignore + } await expectConnectionIsInvalid(conn); }); @@ -584,7 +592,9 @@ void main() { try { await conn.open(); expect(true, false); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignore + } }); }); diff --git a/test/query_reuse_test.dart b/test/query_reuse_test.dart index 306a069..44ebbae 100644 --- a/test/query_reuse_test.dart +++ b/test/query_reuse_test.dart @@ -463,7 +463,9 @@ void main() { try { await connection.query('ljkasd'); expect(true, false); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignore + } expect(getQueryCache(connection).isEmpty, true); }); @@ -478,7 +480,9 @@ void main() { .query(string, substitutionValues: {'i1': 'foo', 'i2': 'bar'}); expect(true, false); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignore + } expect(getQueryCache(connection).length, 0); }); @@ -493,7 +497,9 @@ void main() { .query(string, substitutionValues: {'i1': 'foo', 'i2': 'bar'}); expect(true, false); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignores + } expect(hasCachedQueryNamed(connection, string), false); @@ -528,7 +534,9 @@ void main() { try { await connection.query(string, substitutionValues: {'i': 'foo'}); - } on FormatException {} + } on FormatException { + // ignore + } results = await connection.query(string, substitutionValues: {'i': 2}); expect(results, [ diff --git a/test/timeout_test.dart b/test/timeout_test.dart index 21a5f7f..5ff8f32 100644 --- a/test/timeout_test.dart +++ b/test/timeout_test.dart @@ -26,7 +26,9 @@ void main() { try { await conn.query('SELECT 1', timeoutInSeconds: 1); fail('unreachable'); - } on TimeoutException {} + } on TimeoutException { + // ignore + } expect(f, completes); }); @@ -38,7 +40,9 @@ void main() { await ctx.query('SELECT pg_sleep(2)', timeoutInSeconds: 1); }); fail('unreachable'); - } on TimeoutException {} + } on TimeoutException { + // ignore + } expect(await conn.query('SELECT * from t'), hasLength(0)); }); @@ -52,7 +56,9 @@ void main() { await ctx.query('INSERT INTO t (id) VALUES (1)'); }); fail('unreachable'); - } on TimeoutException {} + } on TimeoutException { + // ignore + } expect(await conn.query('SELECT * from t'), hasLength(0)); }); @@ -63,7 +69,9 @@ void main() { try { await conn.query('SELECT pg_sleep(2)', timeoutInSeconds: 1); fail('unreachable'); - } on TimeoutException {} + } on TimeoutException { + // ignore + } }); test('Query times out, next query in the queue runs', () async { diff --git a/test/transaction_test.dart b/test/transaction_test.dart index a409756..c9f7b8c 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -384,7 +384,9 @@ void main() { await c.query('INSERT INTO t (id) VALUES (1)'); }); expect(true, false); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignore + } final result = await conn.transaction((ctx) async { return await ctx.query('SELECT id FROM t'); @@ -414,7 +416,9 @@ void main() { throw Exception('foo'); }); expect(true, false); - } on Exception {} + } on Exception { + // ignore + } final noRows = await conn.query('SELECT id FROM t'); expect(noRows, []); @@ -465,7 +469,9 @@ void main() { throw Exception('foo'); }); expect(true, false); - } on Exception {} + } on Exception { + // ignore + } final result = await conn.transaction((ctx) async { return await ctx.query('SELECT id FROM t'); @@ -505,7 +511,9 @@ void main() { await c.query('INSERT INTO t (id) VALUES (2)'); }); fail('unreachable'); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignore + } expect(reached, false); final res = await conn.query('SELECT * FROM t'); @@ -524,7 +532,9 @@ void main() { await c.query('INSERT INTO t (id) VALUES (2)'); }); fail('unreachable'); - } on PostgreSQLException {} + } on PostgreSQLException { + // ignore + } final res = await conn.query('SELECT * FROM t'); expect(res, []); From 25782b5d00a08d1b705ff25bdc2d3ac1ac0cd49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sun, 26 May 2019 01:41:36 +0200 Subject: [PATCH 55/73] Refactor mapifyRows (#96) --- lib/src/connection.dart | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index f344782..c22ef4f 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -329,7 +329,7 @@ class Notification { class _OidCache { final _tableOIDNameMap = {}; - Future _resolveOids( + Future _resolveTableNames( PostgreSQLExecutionContext c, List columns) async { //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. // It's not a significant impact here, but an area for optimization. This includes @@ -345,6 +345,10 @@ class _OidCache { if (unresolvedTableOIDs.isNotEmpty) { await _resolveTableOIDs(c, unresolvedTableOIDs); } + + columns.forEach((desc) { + desc.resolvedTableName = _tableOIDNameMap[desc.tableID]; + }); } Future _resolveTableOIDs(PostgreSQLExecutionContext c, List oids) async { @@ -415,7 +419,7 @@ abstract class _PostgreSQLExecutionContextMixin final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); final columns = query.fieldDescriptions; - await _connection._oidCache._resolveOids(this, columns); + await _connection._oidCache._resolveTableNames(this, columns); return _mapifyRows(rows, columns); } @@ -440,23 +444,15 @@ abstract class _PostgreSQLExecutionContextMixin List>> _mapifyRows( List> rows, List columns) { - columns.forEach((desc) { - desc.resolvedTableName = - _connection._oidCache._tableOIDNameMap[desc.tableID]; - }); - - final tableNames = columns.map((c) => c.resolvedTableName).toSet().toList(); return rows.map((row) { - final rowMap = Map>.fromIterable(tableNames, - key: (name) => name as String, value: (_) => {}); - - final iterator = columns.iterator; - row.forEach((column) { - iterator.moveNext(); - rowMap[iterator.current.resolvedTableName][iterator.current.fieldName] = - column; + final rowMap = >{}; + columns.forEach((c) { + rowMap.putIfAbsent(c.resolvedTableName, () => {}); }); - + for (int i = 0; i < columns.length; i++) { + final col = columns[i]; + rowMap[col.resolvedTableName][col.fieldName] = row[i]; + } return rowMap; }).toList(); } From 8c0197809c6eb71cda5db8ffec7f8fd968247c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 27 May 2019 22:26:06 +0200 Subject: [PATCH 56/73] Table OIDs are always resolved to table names. (#97) * allowReuse is set only in the implementation method * Table OIDs are always resolved to table names. * Prevent NPE in certain cases. * Fix map_return_test. * Don't reuse OID selects. * Changelog update. --- CHANGELOG.md | 5 ++ lib/src/connection.dart | 96 ++++++++++++++++++++++++---------- lib/src/execution_context.dart | 8 +-- test/map_return_test.dart | 62 ++++++++++------------ 4 files changed, 106 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25598e8..a17e104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ - `PostgreSQLResult` and `PostgreSQLResultRow` as the return value of a query. - Returned lists are protected with `UnmodifiableListView`. - `PostgreSQLConnection` and `_TransactionProxy` share the OID cache. +- default value for `query(allowReuse = true)` is set only in the implementation method. + +**Breaking behaviour** + +- Table OIDs are always resolved to table names (and not only with mapped queries). ## 1.0.2 - Add connection queue size diff --git a/lib/src/connection.dart b/lib/src/connection.dart index c22ef4f..16754aa 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -328,9 +328,18 @@ class Notification { class _OidCache { final _tableOIDNameMap = {}; + int _queryCount = 0; - Future _resolveTableNames( - PostgreSQLExecutionContext c, List columns) async { + int get queryCount => _queryCount; + + void clear() { + _queryCount = 0; + _tableOIDNameMap.clear(); + } + + Future _resolveTableNames(_PostgreSQLExecutionContextMixin c, + List columns) async { + if (columns == null) return; //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName @@ -351,10 +360,15 @@ class _OidCache { }); } - Future _resolveTableOIDs(PostgreSQLExecutionContext c, List oids) async { + Future _resolveTableOIDs( + _PostgreSQLExecutionContextMixin c, List oids) async { + _queryCount++; final unresolvedIDString = oids.join(','); - final orderedTableNames = await c.query( - "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC"); + final orderedTableNames = await c._query( + "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN ($unresolvedIDString) ORDER BY oid ASC", + allowReuse: false, // inlined OIDs would make it difficult anyway + resolveOids: false, + ); final iterator = oids.iterator; orderedTableNames.forEach((tableName) { @@ -378,11 +392,30 @@ abstract class _PostgreSQLExecutionContextMixin int get queueSize => _queue.length; @override - Future query(String fmtString, - {Map substitutionValues, - bool allowReuse = true, - int timeoutInSeconds}) async { + Future query( + String fmtString, { + Map substitutionValues, + bool allowReuse, + int timeoutInSeconds, + }) => + _query( + fmtString, + substitutionValues: substitutionValues, + allowReuse: allowReuse, + timeoutInSeconds: timeoutInSeconds, + ); + + Future _query( + String fmtString, { + Map substitutionValues, + bool allowReuse, + int timeoutInSeconds, + bool resolveOids, + }) async { + allowReuse ??= true; timeoutInSeconds ??= _connection.queryTimeoutInSeconds; + resolveOids ??= true; + if (_connection.isClosed) { throw PostgreSQLException( 'Attempting to execute query, but connection is not open.'); @@ -395,32 +428,31 @@ abstract class _PostgreSQLExecutionContextMixin } final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); + final columnDescriptions = query.fieldDescriptions; + if (resolveOids) { + await _connection._oidCache._resolveTableNames(this, columnDescriptions); + } + return _PostgreSQLResult( - rows.map((columns) => _PostgreSQLResultRow(columns)).toList()); + columnDescriptions, + rows + .map((columns) => _PostgreSQLResultRow(columnDescriptions, columns)) + .toList()); } @override Future>>> mappedResultsQuery( String fmtString, {Map substitutionValues, - bool allowReuse = true, + bool allowReuse, int timeoutInSeconds}) async { - timeoutInSeconds ??= _connection.queryTimeoutInSeconds; - if (_connection.isClosed) { - throw PostgreSQLException( - 'Attempting to execute query, but connection is not open.'); - } - - final query = Query>>( - fmtString, substitutionValues, _connection, _transaction); - if (allowReuse) { - query.statementIdentifier = _connection._cache.identifierForQuery(query); - } - - final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); - final columns = query.fieldDescriptions; - await _connection._oidCache._resolveTableNames(this, columns); - return _mapifyRows(rows, columns); + final rs = await query( + fmtString, + substitutionValues: substitutionValues, + allowReuse: allowReuse, + timeoutInSeconds: timeoutInSeconds, + ); + return _mapifyRows(rs, rs.columnDescriptions); } @override @@ -486,10 +518,16 @@ abstract class _PostgreSQLExecutionContextMixin class _PostgreSQLResult extends UnmodifiableListView implements PostgreSQLResult { - _PostgreSQLResult(List rows) : super(rows); + @override + final List columnDescriptions; + + _PostgreSQLResult(this.columnDescriptions, List rows) + : super(rows); } class _PostgreSQLResultRow extends UnmodifiableListView implements PostgreSQLResultRow { - _PostgreSQLResultRow(List columns) : super(columns); + final List columnDescriptions; + + _PostgreSQLResultRow(this.columnDescriptions, List columns) : super(columns); } diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 18cdbd8..a65fce5 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -31,7 +31,7 @@ abstract class PostgreSQLExecutionContext { /// the unique identifier to look up reuse information.) You can disable reuse by passing false for [allowReuse]. Future query(String fmtString, {Map substitutionValues, - bool allowReuse = true, + bool allowReuse, int timeoutInSeconds}); /// Executes a query on this context. @@ -83,7 +83,7 @@ abstract class PostgreSQLExecutionContext { Future>>> mappedResultsQuery( String fmtString, {Map substitutionValues, - bool allowReuse = true, + bool allowReuse, int timeoutInSeconds}); } @@ -95,4 +95,6 @@ abstract class PostgreSQLResultRow implements List {} /// The query result. /// /// Rows can be accessed through the [] [List] accessor. -abstract class PostgreSQLResult implements List {} +abstract class PostgreSQLResult implements List { + List get columnDescriptions; +} diff --git a/test/map_return_test.dart b/test/map_return_test.dart index f38d89d..b0135ed 100644 --- a/test/map_return_test.dart +++ b/test/map_return_test.dart @@ -1,12 +1,13 @@ -import 'dart:async'; +import 'dart:mirrors'; + import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; void main() { - InterceptingConnection connection; + PostgreSQLConnection connection; setUp(() async { - connection = InterceptingConnection('localhost', 5432, 'dart_test', + connection = PostgreSQLConnection('localhost', 5432, 'dart_test', username: 'dart', password: 'dart'); await connection.open(); @@ -103,29 +104,21 @@ void main() { }); test('Table names get cached', () async { - final regex = RegExp( - "SELECT relname FROM pg_class WHERE relkind='r' AND oid IN \\(([0-9]*)\\) ORDER BY oid ASC"); - final oids = []; + clearOidQueryCount(connection); + expect(getOidQueryCount(connection), 0); await connection.mappedResultsQuery('SELECT id FROM t'); - expect(connection.queries.length, 1); - var match = regex.firstMatch(connection.queries.first); - oids.add(match.group(1)); - connection.queries.clear(); + expect(getOidQueryCount(connection), 1); await connection.mappedResultsQuery('SELECT id FROM t'); - expect(connection.queries.length, 0); + expect(getOidQueryCount(connection), 1); await connection.mappedResultsQuery( 'SELECT t.id, u.id FROM t LEFT OUTER JOIN u ON t.id=u.t_id'); - expect(connection.queries.length, 1); - match = regex.firstMatch(connection.queries.first); - expect(oids.contains(match.group(1)), false); - oids.add(match.group(1)); - connection.queries.clear(); + expect(getOidQueryCount(connection), 2); await connection.mappedResultsQuery('SELECT u.id FROM u'); - expect(connection.queries.length, 0); + expect(getOidQueryCount(connection), 2); }); test('Non-table mappedResultsQuery succeeds', () async { @@ -138,20 +131,23 @@ void main() { }); } -class InterceptingConnection extends PostgreSQLConnection { - InterceptingConnection(String host, int port, String databaseName, - {String username, String password}) - : super(host, port, databaseName, username: username, password: password); - - List queries = []; - - @override - Future query(String fmtString, - {Map substitutionValues, - bool allowReuse = true, - int timeoutInSeconds}) { - queries.add(fmtString); - return super.query(fmtString, - substitutionValues: substitutionValues, allowReuse: allowReuse); - } +void clearOidQueryCount(PostgreSQLConnection connection) { + final oidCacheMirror = reflect(connection) + .type + .declarations + .values + .firstWhere((DeclarationMirror dm) => + dm.simpleName.toString().contains('_oidCache')); + (reflect(connection).getField(oidCacheMirror.simpleName).reflectee).clear(); +} + +int getOidQueryCount(PostgreSQLConnection connection) { + final oidCacheMirror = reflect(connection) + .type + .declarations + .values + .firstWhere((DeclarationMirror dm) => + dm.simpleName.toString().contains('_oidCache')); + return (reflect(connection).getField(oidCacheMirror.simpleName).reflectee) + .queryCount as int; } From a7bb382e28d270e0b9f1955a37c965acce6375fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 27 May 2019 22:59:34 +0200 Subject: [PATCH 57/73] Proper ColumnDefinition interface (#98) --- CHANGELOG.md | 1 + lib/src/connection.dart | 26 ++++++++++++---------- lib/src/execution_context.dart | 11 +++++++++- lib/src/query.dart | 40 ++++++++++++++++++++++------------ test/query_test.dart | 8 +++++++ 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17e104..7f58609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Updated codebase to Dart 2.2. - `PostgreSQLResult` and `PostgreSQLResultRow` as the return value of a query. - Returned lists are protected with `UnmodifiableListView`. + - Exposing column metadata through `ColumnDescription`. - `PostgreSQLConnection` and `_TransactionProxy` share the OID cache. - default value for `query(allowReuse = true)` is set only in the implementation method. diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 16754aa..7ec2b42 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -337,9 +337,10 @@ class _OidCache { _tableOIDNameMap.clear(); } - Future _resolveTableNames(_PostgreSQLExecutionContextMixin c, + Future> _resolveTableNames( + _PostgreSQLExecutionContextMixin c, List columns) async { - if (columns == null) return; + if (columns == null) return null; //todo (joeconwaystk): If this was a cached query, resolving is table oids is unnecessary. // It's not a significant impact here, but an area for optimization. This includes // assigning resolvedTableName @@ -355,9 +356,9 @@ class _OidCache { await _resolveTableOIDs(c, unresolvedTableOIDs); } - columns.forEach((desc) { - desc.resolvedTableName = _tableOIDNameMap[desc.tableID]; - }); + return columns + .map((c) => c.change(tableName: _tableOIDNameMap[c.tableID])) + .toList(); } Future _resolveTableOIDs( @@ -428,9 +429,10 @@ abstract class _PostgreSQLExecutionContextMixin } final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); - final columnDescriptions = query.fieldDescriptions; + List columnDescriptions = query.fieldDescriptions; if (resolveOids) { - await _connection._oidCache._resolveTableNames(this, columnDescriptions); + columnDescriptions = await _connection._oidCache + ._resolveTableNames(this, columnDescriptions); } return _PostgreSQLResult( @@ -475,15 +477,15 @@ abstract class _PostgreSQLExecutionContextMixin void cancelTransaction({String reason}); List>> _mapifyRows( - List> rows, List columns) { + List> rows, List columns) { return rows.map((row) { final rowMap = >{}; columns.forEach((c) { - rowMap.putIfAbsent(c.resolvedTableName, () => {}); + rowMap.putIfAbsent(c.tableName, () => {}); }); for (int i = 0; i < columns.length; i++) { final col = columns[i]; - rowMap[col.resolvedTableName][col.fieldName] = row[i]; + rowMap[col.tableName][col.columnName] = row[i]; } return rowMap; }).toList(); @@ -519,7 +521,7 @@ abstract class _PostgreSQLExecutionContextMixin class _PostgreSQLResult extends UnmodifiableListView implements PostgreSQLResult { @override - final List columnDescriptions; + final List columnDescriptions; _PostgreSQLResult(this.columnDescriptions, List rows) : super(rows); @@ -527,7 +529,7 @@ class _PostgreSQLResult extends UnmodifiableListView class _PostgreSQLResultRow extends UnmodifiableListView implements PostgreSQLResultRow { - final List columnDescriptions; + final List columnDescriptions; _PostgreSQLResultRow(this.columnDescriptions, List columns) : super(columns); } diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index a65fce5..40e76fb 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -87,6 +87,15 @@ abstract class PostgreSQLExecutionContext { int timeoutInSeconds}); } +/// A description of a column. +abstract class ColumnDescription { + /// The name of the column returned by the query. + String get columnName; + + /// The resolved name of the referenced table. + String get tableName; +} + /// A single row of a query result. /// /// Column values can be accessed through the [] [List] accessor. @@ -96,5 +105,5 @@ abstract class PostgreSQLResultRow implements List {} /// /// Rows can be accessed through the [] [List] accessor. abstract class PostgreSQLResult implements List { - List get columnDescriptions; + List get columnDescriptions; } diff --git a/lib/src/query.dart b/lib/src/query.dart index 2b9c812..2c4fec7 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -223,10 +223,11 @@ class ParameterValue { final int length; } -class FieldDescription { +class FieldDescription implements ColumnDescription { final Converter converter; - final String fieldName; + @override + final String columnName; final int tableID; final int columnID; final int typeID; @@ -234,17 +235,20 @@ class FieldDescription { final int typeModifier; final int formatCode; - String resolvedTableName; + @override + final String tableName; FieldDescription._( - this.converter, - this.fieldName, - this.tableID, - this.columnID, - this.typeID, - this.dataTypeSize, - this.typeModifier, - this.formatCode); + this.converter, + this.columnName, + this.tableID, + this.columnID, + this.typeID, + this.dataTypeSize, + this.typeModifier, + this.formatCode, + this.tableName, + ); factory FieldDescription.read(ByteDataReader reader) { final buf = StringBuffer(); @@ -266,13 +270,21 @@ class FieldDescription { final formatCode = reader.readUint16(); final converter = PostgresBinaryDecoder(typeID); - return FieldDescription._(converter, fieldName, tableID, columnID, typeID, - dataTypeSize, typeModifier, formatCode); + return FieldDescription._( + converter, fieldName, tableID, columnID, typeID, + dataTypeSize, typeModifier, formatCode, + null, // tableName + ); + } + + FieldDescription change({String tableName}) { + return FieldDescription._(converter, columnName, tableID, columnID, typeID, + dataTypeSize, typeModifier, formatCode, tableName ?? this.tableName); } @override String toString() { - return '$fieldName $tableID $columnID $typeID $dataTypeSize $typeModifier $formatCode'; + return '$columnName $tableID $columnID $typeID $dataTypeSize $typeModifier $formatCode'; } } diff --git a/test/query_test.dart b/test/query_test.dart index dcc5164..091e945 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -39,6 +39,9 @@ void main() { expect(result, [expectedRow]); result = await connection.query('select t from t'); + expect(result.columnDescriptions, hasLength(1)); + expect(result.columnDescriptions.single.tableName, 't'); + expect(result.columnDescriptions.single.columnName, 't'); expect(result, [expectedRow]); }); @@ -152,6 +155,11 @@ void main() { {'a': 'b'}, '01234567-89ab-cdef-0123-0123456789ab' ]; + expect(result.columnDescriptions, hasLength(14)); + expect(result.columnDescriptions.first.tableName, 't'); + expect(result.columnDescriptions.first.columnName, 'i'); + expect(result.columnDescriptions.last.tableName, 't'); + expect(result.columnDescriptions.last.columnName, 'u'); expect(result, [expectedRow]); result = await connection.query( 'select i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u from t'); From 3093b7031fdce56285fff9b76a2e495b4d3ec986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 27 May 2019 23:24:58 +0200 Subject: [PATCH 58/73] row-level column map functions (#99) --- CHANGELOG.md | 1 + lib/src/connection.dart | 75 +++++++++++++++++++++++----------- lib/src/execution_context.dart | 12 +++++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f58609..23ddc99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `PostgreSQLResult` and `PostgreSQLResultRow` as the return value of a query. - Returned lists are protected with `UnmodifiableListView`. - Exposing column metadata through `ColumnDescription`. + - row-level `toTableColumnMap` and `toColumnMap` - `PostgreSQLConnection` and `_TransactionProxy` share the OID cache. - default value for `query(allowReuse = true)` is set only in the implementation method. diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 7ec2b42..386559c 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -434,11 +434,12 @@ abstract class _PostgreSQLExecutionContextMixin columnDescriptions = await _connection._oidCache ._resolveTableNames(this, columnDescriptions); } + final metaData = _PostgreSQLResultMetaData(columnDescriptions); return _PostgreSQLResult( - columnDescriptions, + metaData, rows - .map((columns) => _PostgreSQLResultRow(columnDescriptions, columns)) + .map((columns) => _PostgreSQLResultRow(metaData, columns)) .toList()); } @@ -454,7 +455,7 @@ abstract class _PostgreSQLExecutionContextMixin allowReuse: allowReuse, timeoutInSeconds: timeoutInSeconds, ); - return _mapifyRows(rs, rs.columnDescriptions); + return rs.map((row) => row.toTableColumnMap()).toList(); } @override @@ -476,21 +477,6 @@ abstract class _PostgreSQLExecutionContextMixin @override void cancelTransaction({String reason}); - List>> _mapifyRows( - List> rows, List columns) { - return rows.map((row) { - final rowMap = >{}; - columns.forEach((c) { - rowMap.putIfAbsent(c.tableName, () => {}); - }); - for (int i = 0; i < columns.length; i++) { - final col = columns[i]; - rowMap[col.tableName][col.columnName] = row[i]; - } - return rowMap; - }).toList(); - } - Future _enqueue(Query query, {int timeoutInSeconds = 30}) async { if (_queue.add(query)) { _connection._transitionToState(_connection._connectionState.awake()); @@ -518,18 +504,61 @@ abstract class _PostgreSQLExecutionContextMixin Future _onQueryError(Query query, dynamic error, [StackTrace trace]) async {} } +class _PostgreSQLResultMetaData { + final List columnDescriptions; + List _tableNames; + + _PostgreSQLResultMetaData(this.columnDescriptions); + + List get tableNames { + _tableNames ??= + columnDescriptions.map((column) => column.tableName).toSet().toList(); + return _tableNames; + } +} + class _PostgreSQLResult extends UnmodifiableListView implements PostgreSQLResult { - @override - final List columnDescriptions; + final _PostgreSQLResultMetaData _metaData; - _PostgreSQLResult(this.columnDescriptions, List rows) + _PostgreSQLResult(this._metaData, List rows) : super(rows); + + @override + List get columnDescriptions => + _metaData.columnDescriptions; } class _PostgreSQLResultRow extends UnmodifiableListView implements PostgreSQLResultRow { - final List columnDescriptions; + final _PostgreSQLResultMetaData _metaData; + + _PostgreSQLResultRow(this._metaData, List columns) : super(columns); + + @override + List get columnDescriptions => + _metaData.columnDescriptions; + + @override + Map> toTableColumnMap() { + final rowMap = >{}; + _metaData.tableNames.forEach((tableName) { + rowMap[tableName] = {}; + }); + for (int i = 0; i < _metaData.columnDescriptions.length; i++) { + final col = _metaData.columnDescriptions[i]; + rowMap[col.tableName][col.columnName] = this[i]; + } + return rowMap; + } - _PostgreSQLResultRow(this.columnDescriptions, List columns) : super(columns); + @override + Map toColumnMap() { + final rowMap = {}; + for (int i = 0; i < _metaData.columnDescriptions.length; i++) { + final col = _metaData.columnDescriptions[i]; + rowMap[col.columnName] = this[i]; + } + return rowMap; + } } diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 40e76fb..2a62f79 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -99,7 +99,17 @@ abstract class ColumnDescription { /// A single row of a query result. /// /// Column values can be accessed through the [] [List] accessor. -abstract class PostgreSQLResultRow implements List {} +abstract class PostgreSQLResultRow implements List { + List get columnDescriptions; + + /// Returns a two-level map that on the first level contains the resolved + /// table name, and on the second level the column name (or its alias). + Map> toTableColumnMap(); + + /// Returns a single-level map that maps the column name (or its alias) to the + /// value returned on that position. + Map toColumnMap(); +} /// The query result. /// From 4ee5ffd99b3f05528e2a8d53a67baca99488d797 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Wed, 3 Jul 2019 22:50:05 +0200 Subject: [PATCH 59/73] Updated Changelog version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ddc99..4a84872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.0.0-dev1 +## 2.0.0-dev1.0 - Restricted field access on [PostgreSQLConnection]. - Connection-level default query timeout. From 3f1c98e2fe2d7d3b19d43fbebd8f4d5a040419ce Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 2 Aug 2019 23:22:00 +0200 Subject: [PATCH 60/73] Deleting ci/script.sh --- .travis.yml | 1 - ci/script.sh | 9 --------- 2 files changed, 10 deletions(-) delete mode 100644 ci/script.sh diff --git a/.travis.yml b/.travis.yml index f2e6fdb..9d14a9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ before_script: - psql -c 'create user darttrust with createdb;' -U postgres - psql -c 'grant all on database dart_test to darttrust;' -U postgres - pub get -#script: bash ci/script.sh dart_task: - test: --run-skipped -r expanded -j 1 - dartfmt diff --git a/ci/script.sh b/ci/script.sh deleted file mode 100644 index 356e7dc..0000000 --- a/ci/script.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -pub run test -j 1 -r expanded - -# if [[ "$TRAVIS_BRANCH" == "master" ]]; then -# pub global activate -sgit https://github.com/stablekernel/codecov_dart.git -# dart_codecov_generator --report-on=lib/ --verbose --no-html -# fi From 35c6b85368be87061e167143838d1b7f6c6db798 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Wed, 30 Oct 2019 18:10:20 +0100 Subject: [PATCH 61/73] Remove unnecessary const. --- lib/src/constants.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 4d7d872..2e1c0ae 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,7 +1,7 @@ class UTF8ByteConstants { - static const user = const [117, 115, 101, 114, 0]; - static const database = const [100, 97, 116, 97, 98, 97, 115, 101, 0]; - static const clientEncoding = const [ + static const user = [117, 115, 101, 114, 0]; + static const database = [100, 97, 116, 97, 98, 97, 115, 101, 0]; + static const clientEncoding = [ 99, 108, 105, @@ -19,6 +19,6 @@ class UTF8ByteConstants { 103, 0 ]; - static const utf8 = const [85, 84, 70, 56, 0]; - static const timeZone = const [84, 105, 109, 101, 90, 111, 110, 101, 0]; + static const utf8 = [85, 84, 70, 56, 0]; + static const timeZone = [84, 105, 109, 101, 90, 111, 110, 101, 0]; } From 1f379e412ecfac3d8a330541ce1ce20c044890d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Tue, 12 Nov 2019 19:24:11 +0100 Subject: [PATCH 62/73] Update README about the 2.0 branch. (#77) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 263282c..f717a96 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ await connection.transaction((ctx) async { 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. + ## Features and bugs Please file feature requests and bugs at the [issue tracker][tracker]. From 300ff6e0d4ad8cfa442b9b97cabeb1ca8d4e969f Mon Sep 17 00:00:00 2001 From: Leonardo Cecchi Date: Tue, 19 Nov 2019 18:59:53 +0100 Subject: [PATCH 63/73] Fix startup packet length when username is null (#111) `username` is an optional parameter in the connection `open` method but the startup packet length was calculated as the `username` was always not null. This patch fix that behavior, leading to a more clear (backend raised) error message when you don't pass the username field on network connections. --- lib/src/client_messages.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 03910c5..076fc3e 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -53,11 +53,13 @@ class StartupMessage extends ClientMessage { @override void applyToBuffer(ByteDataWriter buffer) { - final fixedLength = 53; - final variableLength = (_username?.utf8Length ?? 0) + - _databaseName.utf8Length + - _timeZone.utf8Length + - 3; + int fixedLength = 48; + int variableLength = _databaseName.utf8Length + _timeZone.utf8Length + 2; + + if (_username != null) { + fixedLength += 5; + variableLength += _username.utf8Length + 1; + } buffer.writeInt32(fixedLength + variableLength); buffer.writeInt32(ClientMessage.ProtocolVersion); From c038deec9476d3841f072a44e67e42fa17ca6058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Tue, 19 Nov 2019 20:20:27 +0100 Subject: [PATCH 64/73] Finalized 2.0.0 release. (#109) * Finalized 2.0.0 release. * Updated changelog --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a84872..265200c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.0.0 + +- Fixed startup packet length when username is null (#111). +- Finalized dev release. + ## 2.0.0-dev1.0 - Restricted field access on [PostgreSQLConnection]. diff --git a/pubspec.yaml b/pubspec.yaml index 63ddf08..52c9b89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 2.0.0-dev1.0 +version: 2.0.0 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: From 0b11ed21e00070609d137fb4a7aeb47845ba9001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 5 Dec 2019 16:49:03 +0100 Subject: [PATCH 65/73] Missing substitution value no longer throws FormatException. (#113) * Missing substitution value no longer throws FormatException. * Fix test, as the table name is not resolveable --- CHANGELOG.md | 5 +++++ lib/src/substituter.dart | 5 +++-- pubspec.yaml | 2 +- test/query_test.dart | 27 ++++++++++----------------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 265200c..67e46aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.1.0 + +- Missing substitution value no longer throws `FormatException`. + [More details in the GitHub issue.](https://github.com/stablekernel/postgresql-dart/issues/57) + ## 2.0.0 - Fixed startup packet length when username is null (#111). diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 35ce77b..632f42c 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -119,8 +119,9 @@ class PostgreSQLFormat { final identifier = PostgreSQLFormatIdentifier(t.buffer.toString()); if (!values.containsKey(identifier.name)) { - throw FormatException( - 'Format string specified identifier with name ${identifier.name}, but key was not present in values. Format string: $fmtString'); + // Format string specified identifier with name ${identifier.name}, + // but key was not present in values. + return t.buffer; } final val = replace(identifier, idx); diff --git a/pubspec.yaml b/pubspec.yaml index 52c9b89..840cc58 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 2.0.0 +version: 2.1.0 author: stable|kernel homepage: https://github.com/stablekernel/postgresql-dart documentation: diff --git a/test/query_test.dart b/test/query_test.dart index 091e945..9627632 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -361,24 +361,17 @@ void main() { }); test( - 'Not enough parameters to support format string throws error prior to sending to server', + 'Missing substitution value does not throw, query is sent to the server without changing that part.', () async { - try { - await connection - .query('INSERT INTO t (i1) values (@i1)', substitutionValues: {}); - expect(true, false); - } on FormatException catch (e) { - expect(e.message, - contains('Format string specified identifier with name i1')); - } - - try { - await connection.query('INSERT INTO t (i1) values (@i1)'); - expect(true, false); - } on FormatException catch (e) { - expect(e.message, - contains('Format string specified identifier with name i1')); - } + final rs1 = await connection + .query('SELECT * FROM (VALUES (\'user@domain.com\')) t1 (col1)'); + expect(rs1.first.toColumnMap(), {'col1': 'user@domain.com'}); + + final rs2 = await connection.query( + 'SELECT * FROM (VALUES (\'user@domain.com\')) t1 (col1) WHERE col1 > @u1', + substitutionValues: {'u1': 'hello@domain.com'}, + ); + expect(rs2.first.toColumnMap(), {'col1': 'user@domain.com'}); }); test('Wrong type for parameter in substitution values fails', () async { From 66db5c0042ce8e34af4f53c19ecc292e6e86c4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 11 Apr 2020 08:59:41 +0200 Subject: [PATCH 66/73] Updated code to match latest pedantic lints and Dart SDK hints. (#115) --- analysis_options.yaml | 1 - lib/src/client_messages.dart | 4 ++-- lib/src/connection.dart | 12 +++++++----- lib/src/execution_context.dart | 4 ++-- lib/src/message_window.dart | 4 ++-- lib/src/query.dart | 4 ++-- lib/src/server_messages.dart | 4 ++-- lib/src/text_codec.dart | 4 ++-- lib/src/transaction_proxy.dart | 2 +- test/framer_test.dart | 2 +- test/notification_test.dart | 4 ++-- test/transaction_test.dart | 4 ++-- 12 files changed, 25 insertions(+), 24 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index ab0596b..860c7d7 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,6 @@ include: package:pedantic/analysis_options.yaml analyzer: errors: - override_on_non_overriding_method: error unused_element: error unused_import: error unused_local_variable: error diff --git a/lib/src/client_messages.dart b/lib/src/client_messages.dart index 076fc3e..e072c07 100644 --- a/lib/src/client_messages.dart +++ b/lib/src/client_messages.dart @@ -53,8 +53,8 @@ class StartupMessage extends ClientMessage { @override void applyToBuffer(ByteDataWriter buffer) { - int fixedLength = 48; - int variableLength = _databaseName.utf8Length + _timeZone.utf8Length + 2; + var fixedLength = 48; + var variableLength = _databaseName.utf8Length + _timeZone.utf8Length + 2; if (_username != null) { fixedLength += 5; diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 386559c..841e012 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -198,8 +198,10 @@ class PostgreSQLConnection extends Object /// If specified, the final `"COMMIT"` query of the transaction will use /// [commitTimeoutInSeconds] as its timeout, otherwise the connection's /// default query timeout will be used. - Future transaction(Future queryBlock(PostgreSQLExecutionContext connection), - {int commitTimeoutInSeconds}) async { + Future transaction( + Future Function(PostgreSQLExecutionContext connection) queryBlock, { + int commitTimeoutInSeconds, + }) async { if (isClosed) { throw PostgreSQLException( 'Attempting to execute query, but connection is not open.'); @@ -429,7 +431,7 @@ abstract class _PostgreSQLExecutionContextMixin } final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); - List columnDescriptions = query.fieldDescriptions; + var columnDescriptions = query.fieldDescriptions; if (resolveOids) { columnDescriptions = await _connection._oidCache ._resolveTableNames(this, columnDescriptions); @@ -545,7 +547,7 @@ class _PostgreSQLResultRow extends UnmodifiableListView _metaData.tableNames.forEach((tableName) { rowMap[tableName] = {}; }); - for (int i = 0; i < _metaData.columnDescriptions.length; i++) { + for (var i = 0; i < _metaData.columnDescriptions.length; i++) { final col = _metaData.columnDescriptions[i]; rowMap[col.tableName][col.columnName] = this[i]; } @@ -555,7 +557,7 @@ class _PostgreSQLResultRow extends UnmodifiableListView @override Map toColumnMap() { final rowMap = {}; - for (int i = 0; i < _metaData.columnDescriptions.length; i++) { + for (var i = 0; i < _metaData.columnDescriptions.length; i++) { final col = _metaData.columnDescriptions[i]; rowMap[col.columnName] = this[i]; } diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 2a62f79..36198db 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -98,7 +98,7 @@ abstract class ColumnDescription { /// A single row of a query result. /// -/// Column values can be accessed through the [] [List] accessor. +/// Column values can be accessed through the `[]` operator. abstract class PostgreSQLResultRow implements List { List get columnDescriptions; @@ -113,7 +113,7 @@ abstract class PostgreSQLResultRow implements List { /// The query result. /// -/// Rows can be accessed through the [] [List] accessor. +/// Rows can be accessed through the `[]` operator. abstract class PostgreSQLResult implements List { List get columnDescriptions; } diff --git a/lib/src/message_window.dart b/lib/src/message_window.dart index 899f5f5..c40129a 100644 --- a/lib/src/message_window.dart +++ b/lib/src/message_window.dart @@ -8,7 +8,7 @@ import 'server_messages.dart'; const int _headerByteSize = 5; final _emptyData = Uint8List(0); -typedef ServerMessage _ServerMessageFn(Uint8List data); +typedef _ServerMessageFn = ServerMessage Function(Uint8List data); Map _messageTypeMap = { 49: (d) => ParseCompleteMessage(), @@ -42,7 +42,7 @@ class MessageFramer { void addBytes(Uint8List bytes) { _reader.add(bytes); - bool evaluateNextMessage = true; + var evaluateNextMessage = true; while (evaluateNextMessage) { evaluateNextMessage = false; diff --git a/lib/src/query.dart b/lib/src/query.dart index 2c4fec7..43e5c58 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -252,7 +252,7 @@ class FieldDescription implements ColumnDescription { factory FieldDescription.read(ByteDataReader reader) { final buf = StringBuffer(); - int byte = 0; + var byte = 0; do { byte = reader.readUint8(); if (byte != 0) { @@ -288,7 +288,7 @@ class FieldDescription implements ColumnDescription { } } -typedef String SQLReplaceIdentifierFunction( +typedef SQLReplaceIdentifierFunction = String Function( PostgreSQLFormatIdentifier identifier, int index); enum PostgreSQLFormatTokenType { text, variable } diff --git a/lib/src/server_messages.dart b/lib/src/server_messages.dart index 9d48d49..4ee1f9a 100644 --- a/lib/src/server_messages.dart +++ b/lib/src/server_messages.dart @@ -167,7 +167,7 @@ class CommandCompleteMessage extends ServerMessage { factory CommandCompleteMessage(Uint8List bytes) { final str = utf8.decode(bytes.sublist(0, bytes.length - 1)); final match = identifierExpression.firstMatch(str); - int rowsAffected = 0; + var rowsAffected = 0; if (match.end < str.length) { rowsAffected = int.parse(str.split(' ').last); } @@ -221,7 +221,7 @@ class UnknownMessage extends ServerMessage { } @override - operator ==(dynamic other) { + bool operator ==(dynamic other) { if (bytes != null) { if (bytes.length != other.bytes.length) { return false; diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index 215f6a4..1cecca0 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -48,8 +48,8 @@ class PostgresTextEncoder extends Converter { final backslashCodeUnit = r'\'.codeUnitAt(0); final quoteCodeUnit = r"'".codeUnitAt(0); - int quoteCount = 0; - int backslashCount = 0; + var quoteCount = 0; + var backslashCount = 0; final it = RuneIterator(text); while (it.moveNext()) { if (it.current == backslashCodeUnit) { diff --git a/lib/src/transaction_proxy.dart b/lib/src/transaction_proxy.dart index 294266d..67ea2b1 100644 --- a/lib/src/transaction_proxy.dart +++ b/lib/src/transaction_proxy.dart @@ -1,6 +1,6 @@ part of postgres.connection; -typedef Future _TransactionQuerySignature( +typedef _TransactionQuerySignature = Future Function( PostgreSQLExecutionContext connection); class _TransactionProxy extends Object diff --git a/test/framer_test.dart b/test/framer_test.dart index 79139e3..77a3996 100644 --- a/test/framer_test.dart +++ b/test/framer_test.dart @@ -197,7 +197,7 @@ Uint8List bufferWithMessages(List> messages) { return Uint8List.fromList(messages.expand((l) => l).toList()); } -flush(MessageFramer framer) { +void flush(MessageFramer framer) { framer.messageQueue.clear(); framer.addBytes(bufferWithMessages([ messageWithBytes([1, 2, 3], 1) diff --git a/test/notification_test.dart b/test/notification_test.dart index 576450f..d6cb5d9 100644 --- a/test/notification_test.dart +++ b/test/notification_test.dart @@ -68,7 +68,7 @@ void main() { test('Notification many channel', () async { final countResponse = {}; - int totalCountResponse = 0; + var totalCountResponse = 0; final finishExecute = Completer(); connection.notifications.listen((msg) { final count = countResponse[msg.channel]; @@ -81,7 +81,7 @@ void main() { final channel2 = 'virtual2'; final notifier = () async { - for (int i = 0; i < 5; i++) { + for (var i = 0; i < 5; i++) { await connection.execute('NOTIFY $channel1;' 'NOTIFY $channel2;'); } diff --git a/test/transaction_test.dart b/test/transaction_test.dart index c9f7b8c..e588032 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -125,7 +125,7 @@ void main() { }); test('May intentionally rollback transaction', () async { - bool reached = false; + var reached = false; await conn.transaction((c) async { await c.query('INSERT INTO t (id) VALUES (1)'); c.cancelTransaction(); @@ -500,7 +500,7 @@ void main() { }); test('Async query failure prevents closure from continuning', () async { - bool reached = false; + var reached = false; try { await conn.transaction((c) async { From 7a88b971f457ebce40e06e2a0f36c2afff2d5988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Wed, 15 Apr 2020 14:06:46 +0200 Subject: [PATCH 67/73] Fix RuneIterator.current use (#122) --- CHANGELOG.md | 4 ++++ lib/src/substituter.dart | 5 +---- pubspec.yaml | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e46aa..a6ae20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.1.1 + +- Fix `RuneIterator.current` use, which no longer returns `null` in 2.8 SDK. + ## 2.1.0 - Missing substitution value no longer throws `FormatException`. diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index 632f42c..e1b1a7c 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -62,8 +62,7 @@ class PostgreSQLFormat { PostgreSQLFormatToken currentPtr; final iterator = RuneIterator(fmtString); - iterator.moveNext(); - while (iterator.current != null) { + while (iterator.moveNext()) { if (currentPtr == null) { if (iterator.current == _atSignCodeUnit) { currentPtr = @@ -105,8 +104,6 @@ class PostgreSQLFormat { items.add(currentPtr); } } - - iterator.moveNext(); } var idx = 1; diff --git a/pubspec.yaml b/pubspec.yaml index 840cc58..a09c233 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,7 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 2.1.0 -author: stable|kernel +version: 2.1.1 homepage: https://github.com/stablekernel/postgresql-dart -documentation: environment: sdk: ">=2.2.0 <3.0.0" From e4ceb60d8ac9090bf3169c63eb1512680a5d6cf9 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Fri, 12 Jun 2020 17:55:15 +0200 Subject: [PATCH 68/73] added unix socket support (#124) --- lib/src/connection.dart | 32 +++++++++++++++++++++++--------- pubspec.yaml | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 841e012..00ffc7f 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -38,13 +38,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 +87,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. @@ -137,8 +145,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) { diff --git a/pubspec.yaml b/pubspec.yaml index a09c233..324c0a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 2.1.1 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 From c9903bc5cf08a65f981f2d640563ccba17c4e515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Fri, 12 Jun 2020 18:05:46 +0200 Subject: [PATCH 69/73] Preparing custom type converters (#131) --- CHANGELOG.md | 4 ++++ lib/src/query.dart | 5 +++-- lib/src/substituter.dart | 2 +- lib/src/text_codec.dart | 35 ++++++++++++++++------------------- pubspec.yaml | 2 +- test/encoding_test.dart | 16 ++++------------ 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ae20e..0fa0488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.2.0 + +- Preparation for custom type converters. + ## 2.1.1 - Fix `RuneIterator.current` use, which no longer returns `null` in 2.8 SDK. diff --git a/lib/src/query.dart b/lib/src/query.dart index 43e5c58..8eb0f85 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -209,8 +209,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); diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index e1b1a7c..0d7f647 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -54,7 +54,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..852f986 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -2,45 +2,42 @@ 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); } + // TODO: use custom type encoders + 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 +82,7 @@ class PostgresTextEncoder extends Converter { return buf.toString(); } - String encodeNumber(num value) { + String _encodeNumber(num value) { if (value.isNaN) { return "'nan'"; } @@ -97,7 +94,7 @@ class PostgresTextEncoder extends Converter { return value.toInt().toString(); } - String encodeDouble(double value) { + String _encodeDouble(double value) { if (value.isNaN) { return "'nan'"; } @@ -109,11 +106,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 +144,7 @@ class PostgresTextEncoder extends Converter { return "'$string'"; } - String encodeJSON(dynamic value) { + String _encodeJSON(dynamic value) { if (value == null) { return 'null'; } diff --git a/pubspec.yaml b/pubspec.yaml index 324c0a4..40c0014 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ 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: diff --git a/test/encoding_test.dart b/test/encoding_test.dart index 877f1cf..edfc81e 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'); From ed1daeb9567d97017ced9af073d91fc64b4f07b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Fri, 12 Jun 2020 18:23:46 +0200 Subject: [PATCH 70/73] Updated changelog for unix socket support (#132) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa0488..48d2ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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 From 29a415589eca11fb46290db2133bf96d0611e0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sat, 27 Jun 2020 22:48:46 +0200 Subject: [PATCH 71/73] Updated readme (#136) --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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 From 64c6ed239ab9374873d96a1fcc63f7811bfbf981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C5=ABras=20=C5=A0lajus?= Date: Mon, 7 Sep 2020 20:14:22 +0300 Subject: [PATCH 72/73] Added rowsAffected to PostgreSQLResult (#143) * Added rowsAffected to PostgreSQLResult which is returned by `PostgreSQLExecutionContext.query` Currently `PostgreSQLExecutionContext.query` does not have a way to know how many rows did that particular query affected. The only way to get that information is to use `PostgreSQLExecutionContext.execute`. Unfortunately `execute` uses slower, less efficient encoding to talk to the PostgreSQL server and does not support all data types that `query` supports. This allows you to use `PostgreSQLExecutionContext.query` for INSERT, UPDATE, DELETE statements and get the affected row count. * Renamed rowsAffected -> affectedRowCount Asked in https://github.com/stablekernel/postgresql-dart/pull/143#discussion_r483989661 * dartfmt Co-authored-by: Arturas Slajus --- lib/src/connection.dart | 21 ++++++++++++++------- lib/src/execution_context.dart | 2 ++ lib/src/query.dart | 15 +++++++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/src/connection.dart b/lib/src/connection.dart index 00ffc7f..ba3455e 100644 --- a/lib/src/connection.dart +++ b/lib/src/connection.dart @@ -444,7 +444,8 @@ abstract class _PostgreSQLExecutionContextMixin query.statementIdentifier = _connection._cache.identifierForQuery(query); } - final rows = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); + final queryResult = + await _enqueue(query, timeoutInSeconds: timeoutInSeconds); var columnDescriptions = query.fieldDescriptions; if (resolveOids) { columnDescriptions = await _connection._oidCache @@ -453,8 +454,9 @@ abstract class _PostgreSQLExecutionContextMixin final metaData = _PostgreSQLResultMetaData(columnDescriptions); return _PostgreSQLResult( + queryResult.affectedRowCount, metaData, - rows + queryResult.value .map((columns) => _PostgreSQLResultRow(metaData, columns)) .toList()); } @@ -476,24 +478,26 @@ abstract class _PostgreSQLExecutionContextMixin @override Future execute(String fmtString, - {Map substitutionValues, int timeoutInSeconds}) { + {Map substitutionValues, int timeoutInSeconds}) async { timeoutInSeconds ??= _connection.queryTimeoutInSeconds; if (_connection.isClosed) { throw PostgreSQLException( 'Attempting to execute query, but connection is not open.'); } - final query = Query( + final query = Query( fmtString, substitutionValues, _connection, _transaction, onlyReturnAffectedRowCount: true); - return _enqueue(query, timeoutInSeconds: timeoutInSeconds); + final result = await _enqueue(query, timeoutInSeconds: timeoutInSeconds); + return result.affectedRowCount; } @override void cancelTransaction({String reason}); - Future _enqueue(Query query, {int timeoutInSeconds = 30}) async { + Future> _enqueue(Query query, + {int timeoutInSeconds = 30}) async { if (_queue.add(query)) { _connection._transitionToState(_connection._connectionState.awake()); @@ -535,9 +539,12 @@ class _PostgreSQLResultMetaData { class _PostgreSQLResult extends UnmodifiableListView implements PostgreSQLResult { + @override + final int affectedRowCount; final _PostgreSQLResultMetaData _metaData; - _PostgreSQLResult(this._metaData, List rows) + _PostgreSQLResult( + this.affectedRowCount, this._metaData, List rows) : super(rows); @override diff --git a/lib/src/execution_context.dart b/lib/src/execution_context.dart index 36198db..c5f0e0e 100644 --- a/lib/src/execution_context.dart +++ b/lib/src/execution_context.dart @@ -115,5 +115,7 @@ abstract class PostgreSQLResultRow implements List { /// /// Rows can be accessed through the `[]` operator. abstract class PostgreSQLResult implements List { + /// How many rows did this query affect? + int get affectedRowCount; List get columnDescriptions; } diff --git a/lib/src/query.dart b/lib/src/query.dart index 8eb0f85..26bd2d5 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -26,7 +26,7 @@ class Query { String statementIdentifier; - Future get future => _onComplete.future; + Future> get future => _onComplete.future; final String statement; final Map substitutionValues; @@ -38,7 +38,7 @@ class Query { CachedQuery cache; - final _onComplete = Completer.sync(); + final _onComplete = Completer>.sync(); List _fieldDescriptions; List get fieldDescriptions => _fieldDescriptions; @@ -154,11 +154,11 @@ class Query { } if (onlyReturnAffectedRowCount) { - _onComplete.complete(rowsAffected as T); + _onComplete.complete(QueryResult(rowsAffected, null)); return; } - _onComplete.complete(rows as T); + _onComplete.complete(QueryResult(rowsAffected, rows as T)); } void completeError(dynamic error, [StackTrace stackTrace]) { @@ -173,6 +173,13 @@ class Query { String toString() => statement; } +class QueryResult { + final int affectedRowCount; + final T value; + + const QueryResult(this.affectedRowCount, this.value); +} + class CachedQuery { CachedQuery(this.preparedStatementName, this.orderedParameters); From fdad1605c1994d3a708b064673b4aea67463b5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Mon, 7 Sep 2020 19:21:32 +0200 Subject: [PATCH 73/73] Preparing release of 2.2.0. (#144) --- CHANGELOG.md | 2 ++ pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d2ede..ca20fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - 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. +- Added rowsAffected to PostgreSQLResult. (Thanks to [arturaz](https://github.com/arturaz), + [#143](https://github.com/stablekernel/postgresql-dart/pull/143)) ## 2.1.1 diff --git a/pubspec.yaml b/pubspec.yaml index 40c0014..48ef18e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports statement reuse and binary protocol. -version: 2.2.0-dev +version: 2.2.0 homepage: https://github.com/stablekernel/postgresql-dart environment: