├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── check-all.sh ├── lib ├── constants.dart ├── pool.dart ├── postgresql.dart ├── postgresql_pool.dart └── src │ ├── buffer.dart │ ├── duration_format.dart │ ├── mock │ ├── mock.dart │ ├── mock_server.dart │ └── mock_socket_server.dart │ ├── pool_impl.dart │ ├── pool_settings_impl.dart │ ├── postgresql_impl │ ├── connection.dart │ ├── constants.dart │ ├── messages.dart │ ├── postgresql_impl.dart │ ├── query.dart │ ├── settings.dart │ └── type_converter.dart │ ├── protocol.dart │ └── substitute.dart ├── pubspec.yaml ├── test ├── buffer_test.dart ├── postgresql_mock_test.dart ├── postgresql_pool_db_test.dart ├── postgresql_pool_test.dart ├── postgresql_test.dart ├── settings_test.dart ├── substitute_test.dart ├── test_config.yaml └── type_converter_test.dart └── tool └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /pubspec.lock 2 | packages 3 | out 4 | *.js 5 | *.deps 6 | test/test_config.yaml 7 | /.pub 8 | /.idea 9 | /.packages 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | before_install: 5 | - export DISPLAY=:99.0 6 | script: 7 | - ./tool/travis.sh 8 | services: 9 | - postgresql 10 | before_script: 11 | - psql -c "create user testdb with password 'password';" -U postgres 12 | - psql -c "create database testdb with owner testdb;" -U postgres 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 0.3.4 2 | 3 | * Update broken crypto dependency. 4 | 5 | #### Version 0.3.3 6 | 7 | * Fix #73 Properly encode/decode connection uris. Thanks to Martin Manev. 8 | * Permit connection without a password. Thanks to Jirka Daněk. 9 | 10 | #### Version 0.3.2 11 | 12 | * Improve handing of datetimes. Thanks to Joe Conway. 13 | * Remove manually cps transformed async code. 14 | * Fix #58: Establish connections concurrently. Thanks to Tom Yeh. 15 | * Fix #67: URI encode db name so spaces can be used in db name. Thanks to Chad Schwendiman. 16 | * Fix #69: Empty connection pool not establishing connections. 17 | 18 | #### Version 0.3.1+1 19 | 20 | * Expose column information via row.getColumns(). Credit to Jesper Håkansson for this change. 21 | 22 | #### Version 0.3.0 23 | 24 | * A new connection pool with more configuration options. 25 | * Support for json and timestamptz types. 26 | * Utc time zone support. 27 | * User customisable type conversions. 28 | * Improved error handling. 29 | * Connection.onClosed has been removed. 30 | * Some api has been renamed, the original names are still functional but marked as deprecated. 31 | * import 'package:postgresql/postgresql_pool.dart' => import 'package:postgresql/pool.dart' 32 | * Pool.destroy() => Pool.stop() 33 | * The constants were upper case and int type. Now typed and lower camel case to match the style guide. 34 | * Connection.unhandled => Connection.messages 35 | * Connection.transactionStatus => Connection.transactionState 36 | 37 | Thanks to Tom Yeh and Petar Sabev for their helpful feedback. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Greg Lowe 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL database driver for Dart 2 | 3 | [![Build Status](https://travis-ci.org/xxgreg/dart_postgresql.svg?branch=master)](https://travis-ci.org/xxgreg/dart_postgresql/) 4 | 5 | ## Basic usage 6 | 7 | ### Obtaining a connection 8 | 9 | ```dart 10 | var uri = 'postgres://username:password@localhost:5432/database'; 11 | connect(uri).then((conn) { 12 | // ... 13 | }); 14 | ``` 15 | 16 | ### SSL connections 17 | 18 | Set the sslmode to require by appending this to the connection uri. This driver only supports sslmode=require, if sslmode is ommitted the driver will always connect without using SSL. 19 | 20 | ```dart 21 | var uri = 'postgres://username:password@localhost:5432/database?sslmode=require'; 22 | connect(uri).then((conn) { 23 | // ... 24 | }); 25 | ``` 26 | 27 | ### Querying 28 | 29 | ```dart 30 | conn.query('select color from crayons').toList().then((rows) { 31 | for (var row in rows) { 32 | print(row.color); // Refer to columns by name, 33 | print(row[0]); // Or by column index. 34 | } 35 | }); 36 | ``` 37 | 38 | ### Executing 39 | 40 | ```dart 41 | conn.execute("update crayons set color = 'pink'").then((rowsAffected) { 42 | print(rowsAffected); 43 | }); 44 | ``` 45 | 46 | ### Query Parameters 47 | 48 | Query parameters can be provided using a map. Strings will be escaped to prevent SQL injection vulnerabilities. 49 | 50 | ```dart 51 | conn.query('select color from crayons where id = @id', {'id': 5}) 52 | .toList() 53 | .then((result) { print(result); }); 54 | 55 | conn.execute('insert into crayons values (@id, @color)', 56 | {'id': 1, 'color': 'pink'}) 57 | .then((_) { print('done.'); }); 58 | ``` 59 | 60 | ### Closing the connection 61 | 62 | You must remember to call Connection.close() when you're done. This won't be 63 | done automatically for you. 64 | 65 | ### Conversion of Postgresql datatypes. 66 | 67 | Below is the mapping from Postgresql types to Dart types. All types which do not have an explicit mapping will be returned as a String in Postgresql's standard text format. This means that it is still possible to handle all types, as you can parse the string yourself. 68 | 69 | ``` 70 | Postgresql type Dart type 71 | boolean bool 72 | int2, int4, int8 int 73 | float4, float8 double 74 | numeric String 75 | timestamp, timestamptz, date Datetime 76 | json, jsonb Map/List 77 | All other types String 78 | ``` 79 | 80 | ### Mapping the results of a query to an object 81 | 82 | ```dart 83 | class Crayon { 84 | String color; 85 | int length; 86 | } 87 | 88 | conn.query('select color, length from crayons') 89 | .map((row) => new Crayon() 90 | ..color = row.color 91 | ..length = row.length) 92 | .toList() 93 | .then((List crayons) { 94 | for (var c in crayons) { 95 | print(c is Crayon); 96 | print(c.color); 97 | print(c.length); 98 | } 99 | }); 100 | ``` 101 | 102 | Or for an immutable object: 103 | 104 | ```dart 105 | class ImmutableCrayon { 106 | ImmutableCrayon(this.color, this.length); 107 | final String color; 108 | final int length; 109 | } 110 | 111 | conn.query('select color, length from crayons') 112 | .map((row) => new ImmutableCrayon(row.color, row.length)) 113 | .toList() 114 | .then((List crayons) { 115 | for (var c in crayons) { 116 | print(c is ImmutableCrayon); 117 | print(c.color); 118 | print(c.length); 119 | } 120 | }); 121 | ``` 122 | 123 | ### Query queueing 124 | 125 | Queries are queued and executed in the order in which they were queued. 126 | 127 | So if you're not concerned about handling errors, you can write code like this: 128 | 129 | ```dart 130 | conn.execute("create table crayons (color text, length int)"); 131 | conn.execute("insert into crayons values ('pink', 5)"); 132 | conn.query("select color from crayons").single.then((crayon) { 133 | print(crayon.color); // prints 'pink' 134 | }); 135 | ``` 136 | 137 | ### Query streaming 138 | 139 | Connection.query() returns a Stream of results. You can use each row as soon as 140 | it is received, or you can wait till they all arrive by calling Stream.toList(). 141 | 142 | ### Connection pooling 143 | 144 | In server applications, a connection pool can be used to avoid the overhead of obtaining a connection for each request. 145 | 146 | ```dart 147 | import 'package:postgresql/pool.dart'; 148 | 149 | main() { 150 | var uri = 'postgres://username:password@localhost:5432/database'; 151 | var pool = new Pool(uri, minConnections: 2, maxConnections: 5); 152 | pool.messages.listen(print); 153 | pool.start().then((_) { 154 | print('Min connections established.'); 155 | pool.connect().then((conn) { // Obtain connection from pool 156 | conn.query("select 'oi';") 157 | .toList() 158 | .then(print) 159 | .then((_) => conn.close()) // Return connection to pool 160 | .catchError((err) => print('Query error: $err')); 161 | }); 162 | }); 163 | } 164 | ``` 165 | 166 | ### Example program 167 | 168 | Add postgresql to your pubspec.yaml file, and run pub install. 169 | 170 | ``` 171 | name: postgresql_example 172 | dependencies: 173 | postgresql: any 174 | ``` 175 | 176 | ```dart 177 | import 'package:postgresql/postgresql.dart'; 178 | 179 | void main() { 180 | var uri = 'postgres://testdb:password@localhost:5432/testdb'; 181 | var sql = "select 'oi'"; 182 | connect(uri).then((conn) { 183 | conn.query(sql).toList() 184 | .then((result) { 185 | print('result: $result'); 186 | }) 187 | .whenComplete(() { 188 | conn.close(); 189 | }); 190 | }); 191 | } 192 | ``` 193 | 194 | ## Testing 195 | 196 | To run the unit tests you will need to create a database, and edit 197 | 'test/config.yaml' accordingly. 198 | 199 | ### Creating a database for testing 200 | 201 | Change to the postgres user and run the administration commands. 202 | ```bash 203 | sudo su postgres 204 | createuser --pwprompt testdb 205 | Enter password for new role: password 206 | Enter it again: password 207 | Shall the new role be a superuser? (y/n) n 208 | Shall the new role be allowed to create databases? (y/n) n 209 | Shall the new role be allowed to create more new roles? (y/n) n 210 | createdb --owner testdb testdb 211 | exit 212 | ``` 213 | 214 | Check that it worked by logging in. 215 | ```bash 216 | psql -h localhost -U testdb -W 217 | ``` 218 | 219 | Enter "\q" to quit from the psql console. 220 | 221 | ## License 222 | 223 | BSD 224 | 225 | ## Links 226 | 227 | http://www.postgresql.org/docs/9.2/static/index.html 228 | http://www.dartlang.org/ 229 | -------------------------------------------------------------------------------- /check-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Abort if non-zero code returned. 4 | set -e 5 | 6 | dartanalyzer lib/postgresql.dart 7 | dartanalyzer lib/pool.dart 8 | dartanalyzer test/postgresql_test.dart 9 | dartanalyzer test/postgresql_pool_test.dart 10 | dartanalyzer test/substitute_test.dart 11 | 12 | 13 | dart --checked test/substitute_test.dart 14 | dart --checked test/settings_test.dart 15 | dart --checked test/type_converter_test.dart 16 | dart --checked test/postgresql_test.dart 17 | #dart --checked test/postgresql_mock_test.dart 18 | #dart --checked test/postgresql_pool_test.dart 19 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | /// Export shorthand constants for enums at top-level. 2 | library postgresql.constants; 3 | 4 | import 'package:postgresql/postgresql.dart'; 5 | import 'package:postgresql/pool.dart'; 6 | 7 | const ConnectionState notConnected = ConnectionState.notConnected; 8 | const ConnectionState socketConnected = ConnectionState.socketConnected; 9 | const ConnectionState authenticating = ConnectionState.authenticating; 10 | const ConnectionState authenticated = ConnectionState.authenticated; 11 | const ConnectionState idle = ConnectionState.idle; 12 | const ConnectionState busy = ConnectionState.busy; 13 | const ConnectionState streaming = ConnectionState.streaming; 14 | const ConnectionState closed = ConnectionState.closed; 15 | 16 | const Isolation readCommitted = Isolation.readCommitted; 17 | const Isolation repeatableRead = Isolation.repeatableRead; 18 | const Isolation serializable = Isolation.serializable; 19 | 20 | const TransactionState unknown = TransactionState.unknown; 21 | const TransactionState none = TransactionState.none; 22 | const TransactionState begun = TransactionState.begun; 23 | const TransactionState error = TransactionState.error; 24 | 25 | const PoolState initial = PoolState.initial; 26 | const PoolState starting = PoolState.starting; 27 | const PoolState startFailed = PoolState.startFailed; 28 | const PoolState running = PoolState.running; 29 | const PoolState stopping = PoolState.stopping; 30 | const PoolState stopped = PoolState.stopped; 31 | 32 | -------------------------------------------------------------------------------- /lib/pool.dart: -------------------------------------------------------------------------------- 1 | library postgresql.pool; 2 | 3 | import 'dart:async'; 4 | import 'package:postgresql/postgresql.dart' as pg; 5 | import 'package:postgresql/src/pool_impl.dart'; 6 | import 'package:postgresql/src/pool_settings_impl.dart'; 7 | 8 | /// A connection pool for PostgreSQL database connections. 9 | abstract class Pool { 10 | 11 | /// See [PoolSettings] for a description of settings. 12 | factory Pool(String databaseUri, 13 | {String poolName, 14 | int minConnections, 15 | int maxConnections, 16 | Duration startTimeout, 17 | Duration stopTimeout, 18 | Duration establishTimeout, 19 | Duration connectionTimeout, 20 | Duration idleTimeout, 21 | Duration maxLifetime, 22 | Duration leakDetectionThreshold, 23 | bool testConnections, 24 | bool restartIfAllConnectionsLeaked, 25 | String applicationName, 26 | String timeZone, 27 | pg.TypeConverter typeConverter}) 28 | 29 | => new PoolImpl(new PoolSettingsImpl.withDefaults( 30 | databaseUri: databaseUri, 31 | poolName: poolName, 32 | minConnections: minConnections, 33 | maxConnections: maxConnections, 34 | startTimeout: startTimeout, 35 | stopTimeout: stopTimeout, 36 | establishTimeout: establishTimeout, 37 | connectionTimeout: connectionTimeout, 38 | idleTimeout: idleTimeout, 39 | maxLifetime: maxLifetime, 40 | leakDetectionThreshold: leakDetectionThreshold, 41 | testConnections: testConnections, 42 | restartIfAllConnectionsLeaked: restartIfAllConnectionsLeaked, 43 | applicationName: applicationName, 44 | timeZone: timeZone), 45 | typeConverter); 46 | 47 | factory Pool.fromSettings(PoolSettings settings, {pg.TypeConverter typeConverter}) 48 | => new PoolImpl(settings, typeConverter); 49 | 50 | Future start(); 51 | Future stop(); 52 | Future connect({String debugName}); 53 | PoolState get state; 54 | Stream get messages; 55 | List get connections; 56 | int get waitQueueLength; 57 | 58 | /// Depreciated. Use [stop]() instead. 59 | @deprecated void destroy(); 60 | } 61 | 62 | 63 | /// Store settings for a PostgreSQL connection pool. 64 | /// 65 | /// An example of loading the connection pool settings from yaml using the 66 | /// [yaml package](https://pub.dartlang.org/packages/yaml): 67 | /// 68 | /// var map = loadYaml(new File('test/test_config.yaml').readAsStringSync()); 69 | /// var settings = new PoolSettings.fromMap(map); 70 | /// var pool = new Pool.fromSettings(settings); 71 | 72 | abstract class PoolSettings { 73 | 74 | factory PoolSettings({ 75 | String databaseUri, 76 | String poolName, 77 | int minConnections, 78 | int maxConnections, 79 | Duration startTimeout, 80 | Duration stopTimeout, 81 | Duration establishTimeout, 82 | Duration connectionTimeout, 83 | Duration idleTimeout, 84 | Duration maxLifetime, 85 | Duration leakDetectionThreshold, 86 | bool testConnections, 87 | bool restartIfAllConnectionsLeaked, 88 | String applicationName, 89 | String timeZone}) = PoolSettingsImpl; 90 | 91 | factory PoolSettings.fromMap(Map map) = PoolSettingsImpl.fromMap; 92 | 93 | String get databaseUri; 94 | 95 | /// Pool name is used in log messages. It is helpful if there are multiple 96 | /// connection pools. Defaults to pgpoolX. 97 | String get poolName; 98 | 99 | /// Minimum number of connections. When the pool is started 100 | /// this is the number of connections that will initially be started. The pool 101 | /// will ensure that this number of connections is always running. In typical 102 | /// production settings, this should be set to be the same size as 103 | /// maxConnections. Defaults to 5. 104 | int get minConnections; 105 | 106 | /// Maximum number of connections. The pool will not exceed 107 | /// this number of database connections. Defaults to 10. 108 | int get maxConnections; 109 | 110 | /// If the pool cannot start within this time then return an 111 | /// error. Defaults to 30 seconds. 112 | Duration get startTimeout; 113 | 114 | /// If when stopping connections are not returned to the pool 115 | /// within this time, then they will be forefully closed. Defaults to 30 116 | /// seconds. 117 | Duration get stopTimeout; 118 | 119 | /// When the pool wants to establish a new database 120 | /// connection and it is not possible to complete within this time then a 121 | /// warning will be logged. Defaults to 30 seconds. 122 | Duration get establishTimeout; 123 | 124 | /// When client code calls Pool.connect(), and a 125 | /// connection does not become available within this time, an error is 126 | /// returned. Defaults to 30 seconds. 127 | Duration get connectionTimeout; 128 | 129 | /// If a connection has not been used for this ammount of time 130 | /// and there are more than the minimum number of connections in the pool, 131 | /// then this connection will be closed. Defaults to 10 minutes. 132 | Duration get idleTimeout; 133 | 134 | /// At the time that a connection is released, if it is older 135 | /// than this time it will be closed. Defaults to 30 minutes. 136 | Duration get maxLifetime; 137 | 138 | /// If a connection is not returned to the pool 139 | /// within this time after being obtained by pool.connect(), the a warning 140 | /// message will be logged. Defaults to null, off by default. This setting is 141 | /// useful for tracking down code which leaks connections by forgetting to 142 | /// call Connection.close() on them. 143 | Duration get leakDetectionThreshold; 144 | 145 | /// Perform a simple query to check if a connection is 146 | /// still valid before returning a connection from pool.connect(). Default is 147 | /// false. 148 | bool get testConnections; 149 | 150 | /// Once the entire pool is full of leaked 151 | /// connections, close them all and restart the minimum number of connections. 152 | /// Defaults to false. This must be used in combination with the leak 153 | /// detection threshold setting. 154 | bool get restartIfAllConnectionsLeaked; 155 | 156 | /// The application name is displayed in the pg_stat_activity view. 157 | String get applicationName; 158 | 159 | /// Care is required when setting the time zone, this is generally not required, 160 | /// the default, if omitted, is to use the server provided default which will 161 | /// typically be localtime or sometimes UTC. Setting the time zone to UTC will 162 | /// override the server provided default and all [DateTime] objects will be 163 | /// returned in UTC. In the case where the application server is on a different 164 | /// host than the database, and the host's [DateTime]s should be in the host's 165 | /// localtime, then set this to the host's local time zone name. On linux 166 | /// systems this can be obtained using: 167 | /// 168 | /// new File('/etc/timezone').readAsStringSync().trim() 169 | /// 170 | String get timeZone; 171 | 172 | Map toMap(); 173 | Map toJson(); 174 | } 175 | 176 | //TODO change to enum once implemented. 177 | class PoolState { 178 | const PoolState(this.name); 179 | final String name; 180 | toString() => name; 181 | 182 | static const PoolState initial = const PoolState('inital'); 183 | static const PoolState starting = const PoolState('starting'); 184 | static const PoolState startFailed = const PoolState('startFailed'); 185 | static const PoolState running = const PoolState('running'); 186 | static const PoolState stopping = const PoolState('stopping'); 187 | static const PoolState stopped = const PoolState('stopped'); 188 | } 189 | 190 | abstract class PooledConnection { 191 | 192 | /// The state of connection in the pool: available, closed, etc. 193 | PooledConnectionState get state; 194 | 195 | /// Time at which the physical connection to the database was established. 196 | DateTime get established; 197 | 198 | /// Time at which the connection was last obtained by a client. 199 | DateTime get obtained; 200 | 201 | /// Time at which the connection was last released by a client. 202 | DateTime get released; 203 | 204 | /// The pid of the postgresql handler. 205 | int get backendPid; 206 | 207 | /// The name passed to connect which is printed in error messages to help 208 | /// with debugging. 209 | String get debugName; 210 | 211 | /// A unique id that updated whenever the connection is obtained. 212 | int get useId; 213 | 214 | /// If a leak detection threshold is set, then this flag will be set on leaked 215 | /// connections. 216 | bool get isLeaked; 217 | 218 | /// The stacktrace at the time pool.connect() was last called. 219 | StackTrace get stackTrace; 220 | 221 | pg.ConnectionState get connectionState; 222 | 223 | String get name; 224 | } 225 | 226 | 227 | //TODO change to enum once implemented. 228 | class PooledConnectionState { 229 | const PooledConnectionState(this.name); 230 | final String name; 231 | toString() => name; 232 | 233 | static const PooledConnectionState connecting = const PooledConnectionState('connecting'); 234 | static const PooledConnectionState available = const PooledConnectionState('available'); 235 | static const PooledConnectionState reserved = const PooledConnectionState('reserved'); 236 | static const PooledConnectionState testing = const PooledConnectionState('testing'); 237 | static const PooledConnectionState inUse = const PooledConnectionState('inUse'); 238 | static const PooledConnectionState closed = const PooledConnectionState('closed'); 239 | } 240 | -------------------------------------------------------------------------------- /lib/postgresql.dart: -------------------------------------------------------------------------------- 1 | library postgresql; 2 | 3 | import 'dart:async'; 4 | import 'package:postgresql/src/postgresql_impl/postgresql_impl.dart' as impl; 5 | 6 | /// Connect to a PostgreSQL database. 7 | /// 8 | /// A uri has the following format: 9 | /// 10 | /// 'postgres://username:password@hostname:5432/database' 11 | /// 12 | /// The application name is displayed in the pg_stat_activity view. This 13 | /// parameter is optional. 14 | /// 15 | /// Care is required when setting the time zone, this is generally not required, 16 | /// the default, if omitted, is to use the server provided default which will 17 | /// typically be localtime or sometimes UTC. Setting the time zone to UTC will 18 | /// override the server provided default and all [DateTime] objects will be 19 | /// returned in UTC. In the case where the application server is on a different 20 | /// host than the database, and the host's [DateTime]s should be in the hosts 21 | /// localtime, then set this to the host's local time zone name. On linux 22 | /// systems this can be obtained using: 23 | /// 24 | /// new File('/etc/timezone').readAsStringSync().trim() 25 | /// 26 | /// The debug name is shown in error messages, this helps tracking down which 27 | /// connection caused an error. 28 | /// 29 | /// The type converter allows the end user to provide their own mapping to and 30 | /// from Dart types to PostgreSQL types. 31 | 32 | Future connect( 33 | String uri, 34 | { Duration connectionTimeout, 35 | String applicationName, 36 | String timeZone, 37 | TypeConverter typeConverter, 38 | String debugName}) 39 | 40 | => impl.ConnectionImpl.connect( 41 | uri, 42 | connectionTimeout: connectionTimeout, 43 | applicationName: applicationName, 44 | timeZone: timeZone, 45 | typeConverter: typeConverter, 46 | getDebugName: () => debugName); 47 | 48 | 49 | /// A connection to a PostgreSQL database. 50 | abstract class Connection { 51 | 52 | /// Queue a sql query to be run, returning a [Stream] of [Row]s. 53 | /// 54 | /// If another query is already in progress, then the query will be queued 55 | /// and executed once the preceding query is complete. 56 | /// 57 | /// The results can be fetched from the [Row]s by column name, or by index. 58 | /// 59 | /// Generally it is best to call [Stream.toList] on the stream and wait for 60 | /// all of the rows to be received. 61 | /// 62 | /// For example: 63 | /// 64 | /// conn.query("select 'pear', 'apple' as a").toList().then((rows) { 65 | /// print(row[0]); 66 | /// print(row.a); 67 | /// }); 68 | /// 69 | /// Values can be substitued into the sql query. If a string contains quotes 70 | /// or other special characters these will be escaped. 71 | /// 72 | /// For example: 73 | /// 74 | /// var a = 'bar'; 75 | /// var b = 42; 76 | /// 77 | /// conn.query("insert into foo_table values (@a, @b);", {'a': a, 'b': b}) 78 | /// .then(...); 79 | /// 80 | /// Or: 81 | /// 82 | /// conn.query("insert into foo_table values (@0, @1);", [a, b]) 83 | /// .then(...); 84 | /// 85 | /// If you need to use an '@' character in your query then you will need to 86 | /// escape it as '@@'. If no values are provided, then there is no need to 87 | /// escape '@' characters. 88 | Stream query(String sql, [values]); 89 | 90 | 91 | /// Queues a command for execution, and when done, returns the number of rows 92 | /// affected by the sql command. Indentical to [query] apart from the 93 | /// information returned. 94 | Future execute(String sql, [values]); 95 | 96 | 97 | /// Allow multiple queries to be run in a transaction. The user must wait for 98 | /// runInTransaction() to complete before making any further queries. 99 | Future runInTransaction(Future operation(), [Isolation isolation]); 100 | 101 | 102 | /// Close the current [Connection]. It is safe to call this multiple times. 103 | /// This will never throw an exception. 104 | void close(); 105 | 106 | /// The server can send errors and notices, or the network can cause errors 107 | /// while the connection is not being used to make a query. These can be 108 | /// caught by listening to the messages stream. See [ClientMessage] and 109 | /// [ServerMessage] for more information. 110 | Stream get messages; 111 | 112 | /// Deprecated. Use messages. 113 | @deprecated Stream get unhandled; 114 | 115 | /// Server configuration parameters such as date format and timezone. 116 | Map get parameters; 117 | 118 | /// The pid of the process the server started to handle this connection. 119 | int get backendPid; 120 | 121 | /// The debug name passed into the connect function. 122 | String get debugName; 123 | 124 | /// The current state of the connection. 125 | ConnectionState get state; 126 | 127 | /// The state of the current transaction. 128 | TransactionState get transactionState; 129 | 130 | /// Deprecated. Use transactionState. 131 | @deprecated TransactionState get transactionStatus; 132 | } 133 | 134 | /// Row allows field values to be retrieved as if they were getters. 135 | /// 136 | /// c.query("select 'blah' as my_field") 137 | /// .single 138 | /// .then((row) => print(row.my_field)); 139 | /// 140 | /// Or by index. 141 | /// 142 | /// c.query("select 'blah'") 143 | /// .single 144 | /// .then((row) => print(row[0])); 145 | /// 146 | @proxy 147 | abstract class Row { 148 | 149 | /// Get a column value by column index (zero based). 150 | operator[] (int i); 151 | 152 | /// Iterate through column names and values. 153 | void forEach(void f(String columnName, columnValue)); 154 | 155 | /// An unmodifiable list of column values. 156 | List toList(); 157 | 158 | /// An unmodifiable map of column names and values. 159 | Map toMap(); 160 | 161 | List getColumns(); 162 | } 163 | 164 | abstract class Column { 165 | int get index; 166 | String get name; 167 | 168 | int get fieldId; 169 | int get tableColNo; 170 | int get fieldType; 171 | int get dataSize; 172 | int get typeModifier; 173 | int get formatCode; 174 | 175 | bool get isBinary; 176 | } 177 | 178 | 179 | /// The server can send errors and notices, or the network can cause errors 180 | /// while the connection is not being used to make a query. See 181 | /// [ClientMessage] and [ServerMessage] for more information. 182 | abstract class Message { 183 | 184 | /// Returns true if this is an error, otherwise it is a server-side notice, 185 | /// or logging. 186 | bool get isError; 187 | 188 | /// For a [ServerMessage] from an English localized database the field 189 | /// contents are ERROR, FATAL, or PANIC, for an error message. Otherwise in 190 | /// a notice message they are 191 | /// WARNING, NOTICE, DEBUG, INFO, or LOG. 192 | 193 | String get severity; 194 | 195 | /// A human readible error message, typically one line. 196 | String get message; 197 | 198 | /// An identifier for the connection. Useful for logging messages in a 199 | /// connection pool. 200 | String get connectionName; 201 | } 202 | 203 | /// An error or warning generated by the client. 204 | abstract class ClientMessage implements Message { 205 | 206 | factory ClientMessage( 207 | {bool isError, 208 | String severity, 209 | String message, 210 | String connectionName, 211 | exception, 212 | StackTrace stackTrace}) = impl.ClientMessageImpl; 213 | 214 | final exception; 215 | 216 | final StackTrace stackTrace; 217 | } 218 | 219 | /// Represents an error or a notice sent from the postgresql server. 220 | abstract class ServerMessage implements Message { 221 | 222 | /// Returns true if this is an error, otherwise it is a server-side notice. 223 | bool get isError; 224 | 225 | /// All of the information returned from the server. 226 | Map get fields; 227 | 228 | /// An identifier for the connection. Useful for logging messages in a 229 | /// connection pool. 230 | String get connectionName; 231 | 232 | /// For a [ServerMessage] from an English localized database the field 233 | /// contents are ERROR, FATAL, or PANIC, for an error message. Otherwise in 234 | /// a notice message they are 235 | /// WARNING, NOTICE, DEBUG, INFO, or LOG. 236 | String get severity; 237 | 238 | /// A PostgreSQL error code. 239 | /// See http://www.postgresql.org/docs/9.2/static/errcodes-appendix.html 240 | String get code; 241 | 242 | /// A human readible error message, typically one line. 243 | String get message; 244 | 245 | /// More detailed information. 246 | String get detail; 247 | 248 | String get hint; 249 | 250 | /// The position as an index into the original query string where the syntax 251 | /// error was found. The first character has index 1, and positions are 252 | /// measured in characters not bytes. If the server does not supply a 253 | /// position this field is null. 254 | String get position; 255 | 256 | String get internalPosition; 257 | String get internalQuery; 258 | String get where; 259 | String get schema; 260 | String get table; 261 | String get column; 262 | String get dataType; 263 | String get constraint; 264 | String get file; 265 | String get line; 266 | String get routine; 267 | 268 | } 269 | 270 | 271 | /// By implementing this class and passing it to connect(), it is possible to 272 | /// provide a customised handling of the Dart type encoding and PostgreSQL type 273 | /// decoding. 274 | abstract class TypeConverter { 275 | 276 | factory TypeConverter() = impl.DefaultTypeConverter; 277 | 278 | /// Returns all results in the raw postgresql string format without conversion. 279 | factory TypeConverter.raw() = impl.RawTypeConverter; 280 | 281 | /// Convert an object to a string representation to use in a sql query. 282 | /// Be very careful to escape your strings correctly. If you get this wrong 283 | /// you will introduce a sql injection vulnerability. Consider using the 284 | /// provided [encodeString] function. 285 | String encode(value, String type, {getConnectionName()}); 286 | 287 | /// Convert a string recieved from the database into a dart object. 288 | Object decode(String value, int pgType, 289 | {bool isUtcTimeZone, getConnectionName()}); 290 | } 291 | 292 | /// Escape strings to a postgresql string format. i.e. E'str\'ing' 293 | String encodeString(String s) => impl.encodeString(s); 294 | 295 | 296 | //TODO change to enum once implemented. 297 | 298 | /// The current state of a connection. 299 | class ConnectionState { 300 | final String _name; 301 | const ConnectionState(this._name); 302 | String toString() => _name; 303 | 304 | static const ConnectionState notConnected = const ConnectionState('notConnected'); 305 | static const ConnectionState socketConnected = const ConnectionState('socketConnected'); 306 | static const ConnectionState authenticating = const ConnectionState('authenticating'); 307 | static const ConnectionState authenticated = const ConnectionState('authenticated'); 308 | static const ConnectionState idle = const ConnectionState('idle'); 309 | static const ConnectionState busy = const ConnectionState('busy'); 310 | 311 | // state is called "ready" in libpq. Doesn't make sense in a non-blocking impl. 312 | static const ConnectionState streaming = const ConnectionState('streaming'); 313 | static const ConnectionState closed = const ConnectionState('closed'); 314 | } 315 | 316 | //TODO change to enum once implemented. 317 | 318 | /// Describes whether the a connection is participating in a transaction, and 319 | /// if the transaction has failed. 320 | class TransactionState { 321 | final String _name; 322 | 323 | const TransactionState(this._name); 324 | 325 | String toString() => _name; 326 | 327 | /// Directly after sending a query the transaction state is unknown, as the 328 | /// query may change the transaction state. Wait until the query is completed 329 | /// to query the transaction state. 330 | static const TransactionState unknown = const TransactionState('unknown'); 331 | 332 | /// The current session has not opened a transaction. 333 | static const TransactionState none = const TransactionState('none'); 334 | 335 | /// The current session has an open transaction. 336 | static const TransactionState begun = const TransactionState('begun'); 337 | 338 | /// A transaction was opened on the current session, but an error occurred. 339 | /// In this state all futher commands will be ignored until a rollback is 340 | /// issued. 341 | static const TransactionState error = const TransactionState('error'); 342 | } 343 | 344 | //TODO change to enum once implemented. 345 | 346 | /// See http://www.postgresql.org/docs/9.3/static/transaction-iso.html 347 | class Isolation { 348 | final String _name; 349 | const Isolation(this._name); 350 | String toString() => _name; 351 | 352 | static const Isolation readCommitted = const Isolation('readCommitted'); 353 | static const Isolation repeatableRead = const Isolation('repeatableRead'); 354 | static const Isolation serializable = const Isolation('serializable'); 355 | } 356 | 357 | 358 | @deprecated const Isolation READ_COMMITTED = Isolation.readCommitted; 359 | @deprecated const Isolation REPEATABLE_READ = Isolation.repeatableRead; 360 | @deprecated const Isolation SERIALIZABLE = Isolation.serializable; 361 | 362 | @deprecated const TRANSACTION_BEGUN = TransactionState.begun; 363 | @deprecated const TRANSACTION_ERROR = TransactionState.error; 364 | @deprecated const TRANSACTION_NONE = TransactionState.none; 365 | @deprecated const TRANSACTION_UNKNOWN = TransactionState.unknown; 366 | 367 | 368 | class PostgresqlException implements Exception { 369 | 370 | PostgresqlException(this.message, this.connectionName, {this.serverMessage, this.exception}); 371 | 372 | final String message; 373 | 374 | /// Note the connection name can be null in some cases when thrown by pool. 375 | final String connectionName; 376 | 377 | final ServerMessage serverMessage; 378 | 379 | /// Note may be null. 380 | final exception; 381 | 382 | String toString() { 383 | if (serverMessage != null) return serverMessage.toString(); 384 | return connectionName == null ? message : '$connectionName $message'; 385 | } 386 | } 387 | 388 | /// Settings can be used to create a postgresql uri for use in the [connect] 389 | /// function. 390 | /// 391 | /// An example of loading the connection settings from yaml using the 392 | /// [yaml package](https://pub.dartlang.org/packages/yaml): 393 | /// 394 | /// var map = loadYaml(new File('db.yaml').readAsStringSync()); 395 | /// var settings = new Settings.fromMap(map); 396 | /// var uri = settings.toUri(); 397 | /// connect(uri).then(...); 398 | /// 399 | abstract class Settings { 400 | 401 | /// The default port used by a PostgreSQL server. 402 | static const num defaultPort = 5432; 403 | 404 | factory Settings(String host, int port, String user, String password, 405 | String database, {bool requireSsl}) = impl.SettingsImpl; 406 | 407 | /// Parse a PostgreSQL URI string. 408 | factory Settings.fromUri(String uri) = impl.SettingsImpl.fromUri; 409 | 410 | /// Read settings from a map and set default values for unspecified values. 411 | /// Throws [PostgresqlException] when a required setting is not provided. 412 | factory Settings.fromMap(Map config) = impl.SettingsImpl.fromMap; 413 | String get host; 414 | int get port; 415 | String get user; 416 | String get password; 417 | String get database; 418 | bool get requireSsl; 419 | 420 | /// Return a connection URI. 421 | String toUri(); 422 | Map toMap(); 423 | String toString(); 424 | } 425 | -------------------------------------------------------------------------------- /lib/postgresql_pool.dart: -------------------------------------------------------------------------------- 1 | @deprecated 2 | // Deprecated, instead use: import 'package:postgresql/pool.dart';. 3 | library postgresql.postgresql_pool; 4 | 5 | export 'package:postgresql/pool.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/buffer.dart: -------------------------------------------------------------------------------- 1 | library postgresql.buffer; 2 | 3 | import 'dart:collection'; 4 | import 'dart:convert'; 5 | 6 | // TODO Plenty of oportunity for optimisation here. This is just a quick and simple, 7 | // implementation. 8 | // Switch to use new core classes such as ChunkedConversionSink 9 | // Example here: https://www.dartlang.org/articles/converters-and-codecs/ 10 | class Buffer { 11 | 12 | Buffer(this._createException); 13 | 14 | Function _createException; 15 | 16 | int _position = 0; 17 | final Queue> _queue = new Queue>(); 18 | 19 | int _bytesRead = 0; 20 | int get bytesRead => _bytesRead; 21 | 22 | int get bytesAvailable => _queue.fold(0, (len, buffer) => len + buffer.length) - _position; 23 | 24 | int readByte() { 25 | if (_queue.isEmpty) 26 | throw _createException("Attempted to read from an empty buffer."); 27 | 28 | int byte = _queue.first[_position]; 29 | 30 | _position++; 31 | if (_position >= _queue.first.length) { 32 | _queue.removeFirst(); 33 | _position = 0; 34 | } 35 | 36 | _bytesRead++; 37 | 38 | return byte; 39 | } 40 | 41 | int readInt16() { 42 | int a = readByte(); 43 | int b = readByte(); 44 | 45 | assert(a < 256 && b < 256 && a >= 0 && b >= 0); 46 | int i = (a << 8) | b; 47 | 48 | if (i >= 0x8000) 49 | i = -0x10000 + i; 50 | 51 | return i; 52 | } 53 | 54 | int readInt32() { 55 | int a = readByte(); 56 | int b = readByte(); 57 | int c = readByte(); 58 | int d = readByte(); 59 | 60 | assert(a < 256 && b < 256 && c < 256 && d < 256 && a >= 0 && b >= 0 && c >= 0 && d >= 0); 61 | int i = (a << 24) | (b << 16) | (c << 8) | d; 62 | 63 | if (i >= 0x80000000) 64 | i = -0x100000000 + i; 65 | 66 | return i; 67 | } 68 | 69 | List readBytes(int bytes) { 70 | var list = new List(bytes); 71 | for (int i = 0; i < bytes; i++) { 72 | list[i] = readByte(); 73 | } 74 | return list; 75 | } 76 | 77 | /// Read a fixed length utf8 string with a known size in bytes. 78 | //TODO This is a hot method find a way to optimise this. 79 | // Switch to use new core classes such as ChunkedConversionSink 80 | // Example here: https://www.dartlang.org/articles/converters-and-codecs/ 81 | String readUtf8StringN(int size) => UTF8.decode(readBytes(size)); 82 | 83 | 84 | /// Read a zero terminated utf8 string. 85 | String readUtf8String(int maxSize) { 86 | //TODO Optimise this. Though note it isn't really a hot function. The most 87 | // performance critical place that this is used is in reading column headers 88 | // which are short, and only once per query. 89 | var bytes = new List(); 90 | int c, i = 0; 91 | while ((c = readByte()) != 0) { 92 | if (i > maxSize) throw _createException('Max size exceeded while reading string: $maxSize.'); 93 | bytes.add(c); 94 | } 95 | return UTF8.decode(bytes); 96 | } 97 | 98 | void append(List data) { 99 | if (data == null || data.isEmpty) 100 | throw new Exception("Attempted to append null or empty list."); 101 | 102 | _queue.addLast(data); 103 | } 104 | } 105 | 106 | //TODO switch to using the new ByteBuilder class. 107 | class MessageBuffer { 108 | List _buffer = new List(); 109 | List get buffer => _buffer; 110 | 111 | void addByte(int byte) { 112 | assert(byte >= 0 && byte < 256); 113 | _buffer.add(byte); 114 | } 115 | 116 | void addInt16(int i) { 117 | assert(i >= -32768 && i <= 32767); 118 | 119 | if (i < 0) 120 | i = 0x10000 + i; 121 | 122 | int a = (i >> 8) & 0x00FF; 123 | int b = i & 0x00FF; 124 | 125 | _buffer.add(a); 126 | _buffer.add(b); 127 | } 128 | 129 | void addInt32(int i) { 130 | assert(i >= -2147483648 && i <= 2147483647); 131 | 132 | if (i < 0) 133 | i = 0x100000000 + i; 134 | 135 | int a = (i >> 24) & 0x000000FF; 136 | int b = (i >> 16) & 0x000000FF; 137 | int c = (i >> 8) & 0x000000FF; 138 | int d = i & 0x000000FF; 139 | 140 | _buffer.add(a); 141 | _buffer.add(b); 142 | _buffer.add(c); 143 | _buffer.add(d); 144 | } 145 | 146 | void addUtf8String(String s) { 147 | //Postgresql server must be configured to accept UTF8 - this is the default. 148 | _buffer.addAll(UTF8.encode(s)); 149 | addByte(0); 150 | } 151 | 152 | void setLength({bool startup: false}) { 153 | int offset = 0; 154 | int i = _buffer.length; 155 | 156 | if (!startup) { 157 | offset = 1; 158 | i -= 1; 159 | } 160 | 161 | _buffer[offset] = (i >> 24) & 0x000000FF; 162 | _buffer[offset + 1] = (i >> 16) & 0x000000FF; 163 | _buffer[offset + 2] = (i >> 8) & 0x000000FF; 164 | _buffer[offset + 3] = i & 0x000000FF; 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /lib/src/duration_format.dart: -------------------------------------------------------------------------------- 1 | library postgresql.duration_format; 2 | 3 | class DurationFormat { 4 | 5 | DurationFormat() 6 | : _approx = false, _threshold = 0; 7 | 8 | DurationFormat.approximate([int threshold = 5]) 9 | : _approx = true, _threshold = threshold; 10 | 11 | final bool _approx; 12 | final int _threshold; 13 | 14 | Duration parse(String s, {onError(String s)}) { 15 | if (s == null) throw new ArgumentError.notNull('string'); 16 | 17 | ex() => new FormatException('Cannot parse string as duration: "$s".'); 18 | fail(s) => onError == null ? throw ex() : onError(s); 19 | 20 | parsePrefix(s, [int suffixLen = 1]) 21 | => int.parse(s.substring(0, s.length-suffixLen), onError: fail); 22 | 23 | if (s.endsWith('d')) return new Duration(days: parsePrefix(s)); 24 | if (s.endsWith('h')) return new Duration(hours: parsePrefix(s)); 25 | if (s.endsWith('m')) return new Duration(minutes: parsePrefix(s)); 26 | if (s.endsWith('s')) return new Duration(seconds: parsePrefix(s)); 27 | if (s.endsWith('ms')) return new Duration(milliseconds: parsePrefix(s, 2)); 28 | if (s.endsWith('us')) return new Duration(microseconds: parsePrefix(s, 2)); 29 | 30 | throw ex(); 31 | } 32 | 33 | String format(Duration d) { 34 | 35 | if (d == null) throw new ArgumentError.notNull('duration'); 36 | 37 | if (_approx) d = _approximate(d); 38 | 39 | if (d.inMicroseconds == 0) 40 | return '0s'; 41 | 42 | if (d.inMicroseconds % Duration.MICROSECONDS_PER_MILLISECOND != 0) 43 | return '${d.inMicroseconds}us'; 44 | 45 | if (d.inMilliseconds % Duration.MILLISECONDS_PER_SECOND != 0) 46 | return '${d.inMilliseconds}ms'; 47 | 48 | if (d.inSeconds % Duration.SECONDS_PER_MINUTE != 0) 49 | return '${d.inSeconds}s'; 50 | 51 | if (d.inMinutes % Duration.MINUTES_PER_HOUR != 0) 52 | return '${d.inMinutes}m'; 53 | 54 | if (d.inHours % Duration.HOURS_PER_DAY != 0) 55 | return '${d.inHours}h'; 56 | 57 | return '${d.inDays}d'; 58 | } 59 | 60 | // Round up to the nearest unit. 61 | Duration _approximate(Duration d) { 62 | if (d.inMicroseconds == 0) return d; 63 | 64 | if (d > new Duration(days: _threshold)) 65 | return new Duration(days: d.inDays); 66 | 67 | if (d > new Duration(hours: _threshold)) 68 | return new Duration(hours: d.inHours); 69 | 70 | if (d > new Duration(minutes: _threshold)) 71 | return new Duration(minutes: d.inMinutes); 72 | 73 | if (d > new Duration(seconds: _threshold)) 74 | return new Duration(seconds: d.inSeconds); 75 | 76 | if (d > new Duration(milliseconds: _threshold)) 77 | return new Duration(milliseconds: d.inMilliseconds); 78 | 79 | return new Duration(microseconds: d.inMicroseconds); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/mock/mock.dart: -------------------------------------------------------------------------------- 1 | /// Library used for testing the postgresql connection pool. 2 | library postgresql.mock; 3 | 4 | import 'dart:async'; 5 | import 'dart:collection'; 6 | import 'dart:io'; 7 | import 'package:postgresql/constants.dart'; 8 | import 'package:postgresql/postgresql.dart' as pg; 9 | import 'package:postgresql/src/pool_impl.dart' as pi; 10 | import 'package:postgresql/src/postgresql_impl/postgresql_impl.dart'; 11 | import 'package:func/func.dart'; 12 | part 'mock_server.dart'; 13 | part 'mock_socket_server.dart'; 14 | 15 | Function mockLogger; 16 | 17 | void _log(msg) { if (mockLogger != null) mockLogger(msg); } 18 | 19 | 20 | const toClient = 'to-client'; 21 | const toServer = 'to-server'; 22 | const clientClosed = 'client-closed'; 23 | const clientDestroyed = 'client-destroyed'; 24 | const serverClosed = 'server-closed'; 25 | const socketError = 'socket-error'; 26 | 27 | class Packet { 28 | Packet(this.direction, this.data); 29 | var direction; 30 | List data; 31 | } 32 | 33 | 34 | abstract class MockServer { 35 | 36 | factory MockServer() = MockServerImpl; 37 | 38 | // Starts a mock server using a real socket. 39 | static Future startSocketServer([int port]) 40 | => MockSocketServerImpl.start(port); 41 | 42 | Future connect(); 43 | 44 | List get backends; 45 | 46 | Future waitForConnect(); 47 | 48 | void stop(); 49 | } 50 | 51 | 52 | // For each call to MockServer.connect(), one backend is created. 53 | abstract class Backend { 54 | 55 | List get log; 56 | List> get received; 57 | bool get isClosed; 58 | bool get isDestroyed; //FIXME do I need both this and close? Is this client side specific? 59 | 60 | /// Send data over the socket from the mock server to the client listening 61 | /// on the socket. 62 | void sendToClient(List data); 63 | 64 | /// Clear out received data. 65 | void clear(); 66 | 67 | /// Server closes the connection. 68 | void close(); 69 | 70 | // This is can only be used for a MockServer, not a MockSocketServer. 71 | //FIXME can other types of exception be received? 72 | /// Client receives socket error. 73 | void socketException(String msg); 74 | 75 | Future waitForClient(); 76 | } 77 | 78 | 79 | 80 | Stream queryResults(List rows) => new Stream.fromIterable( 81 | rows.map((row) { 82 | if (row is Map) return new MockRow.fromMap(row); 83 | if (row is List) return new MockRow.fromList(row); 84 | throw 'Expected list or map, got: ${row.runtimeType}.'; 85 | })); 86 | 87 | int _sequence = 1; 88 | 89 | 90 | 91 | class MockConnection implements pg.Connection { 92 | 93 | pg.ConnectionState state = pg.ConnectionState.idle; 94 | pg.TransactionState transactionState = none; 95 | pg.TransactionState transactionStatus = none; 96 | 97 | Map parameters = {}; 98 | 99 | int backendPid = 42; 100 | 101 | String debugName = 'pgconn'; 102 | 103 | Stream query(String sql, [values]) { 104 | _log('query("$sql")'); 105 | if (sql == 'select pg_backend_pid()') return queryResults([[_sequence++]]); 106 | if (sql == 'select true') return queryResults([[true]]); 107 | // TODO allow adding json query results. i.e. [[42]] 108 | if (sql.startsWith('mock timeout')) { 109 | var re = new RegExp(r'mock timeout (\d+)'); 110 | var match = re.firstMatch(sql); 111 | int delay = match == null ? 10 : int.parse(match[1]); 112 | return new Stream.fromFuture( 113 | new Future.delayed(new Duration(seconds: delay))); 114 | } 115 | return onQuery(sql, values); 116 | } 117 | 118 | Func2> onQuery = (sql, values) {}; 119 | 120 | Future execute(String sql, [values]) { 121 | _log('execute("$sql")'); 122 | return onExecute(sql, values); 123 | } 124 | 125 | Func2> onExecute = (sql, values) {}; 126 | 127 | 128 | void close() { 129 | _log('close'); 130 | onClose(); 131 | } 132 | 133 | Function onClose = () {}; 134 | 135 | 136 | Stream get messages => messagesController.stream; 137 | Stream get unhandled => messages; 138 | StreamController messagesController = new StreamController.broadcast(); 139 | 140 | Future runInTransaction(Future operation(), [pg.Isolation isolation]) 141 | => throw new UnimplementedError(); 142 | 143 | } 144 | 145 | 146 | abstract class MockRow implements pg.Row { 147 | factory MockRow.fromList(List list) => new _ListMockRow(list); 148 | factory MockRow.fromMap(LinkedHashMap map) => new _MapMockRow(map); 149 | } 150 | 151 | @proxy 152 | class _MapMockRow implements MockRow { 153 | 154 | _MapMockRow(this._values); 155 | 156 | final LinkedHashMap _values; 157 | 158 | operator [](int i) { 159 | return _values.values.elementAt(i); 160 | } 161 | 162 | @override 163 | void forEach(void f(String columnName, columnValue)) { 164 | _values.forEach(f); 165 | } 166 | 167 | noSuchMethod(Invocation invocation) { 168 | var name = invocation.memberName; 169 | if (invocation.isGetter) { 170 | return _values[name]; 171 | } 172 | super.noSuchMethod(invocation); 173 | } 174 | 175 | String toString() => _values.values.toString(); 176 | } 177 | 178 | class _ListMockRow implements MockRow { 179 | 180 | _ListMockRow(List values, [List columnNames]) 181 | : _values = values, 182 | _columnNames = columnNames == null 183 | ? new Iterable.generate(values.length, (i) => i.toString()).toList() 184 | : columnNames; 185 | 186 | final List _values; 187 | final List _columnNames; 188 | 189 | operator [](int i) { 190 | return _values.elementAt(i); 191 | } 192 | 193 | @override 194 | void forEach(void f(String columnName, columnValue)) { 195 | toMap().forEach(f); 196 | } 197 | 198 | String toString() => _values.toString(); 199 | 200 | List toList() => new UnmodifiableListView(_values); 201 | 202 | Map toMap() => new Map.fromIterables(_columnNames, _values); 203 | 204 | List getColumns() { 205 | throw new UnimplementedError(); 206 | } 207 | } 208 | 209 | 210 | pi.ConnectionFactory mockConnectionFactory([Future mockConnect()]) { 211 | if (mockConnect == null) 212 | mockConnect = () => new Future.value(new MockConnection()); 213 | return 214 | (String uri, 215 | {Duration connectionTimeout, 216 | String applicationName, 217 | String timeZone, 218 | pg.TypeConverter typeConverter, 219 | String getDebugName(), 220 | Future mockSocketConnect(String host, int port)}) 221 | => mockConnect(); 222 | } 223 | 224 | -------------------------------------------------------------------------------- /lib/src/mock/mock_server.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.mock; 2 | 3 | class MockServerBackendImpl implements Backend { 4 | MockServerBackendImpl() { 5 | mocket.onClose = () { 6 | _isClosed = true; 7 | log.add(new Packet(clientClosed, [])); 8 | }; 9 | 10 | mocket.onDestroy = () { 11 | _isClosed = true; 12 | _isDestroyed = true; 13 | log.add(new Packet(clientDestroyed, [])); 14 | }; 15 | 16 | mocket.onAdd = (List data) { 17 | received.add(data); 18 | log.add(new Packet(toServer, data)); 19 | if (_waitForClient != null) { 20 | _waitForClient.complete(); 21 | _waitForClient = null; 22 | } 23 | }; 24 | 25 | mocket.onError = (err, [st]) { 26 | throw err; 27 | }; 28 | } 29 | 30 | final Mocket mocket = new Mocket(); 31 | 32 | final List log = new List(); 33 | final List> received = new List>(); 34 | 35 | bool _isClosed = true; 36 | bool _isDestroyed = true; 37 | bool get isClosed => _isClosed; 38 | bool get isDestroyed => _isDestroyed; 39 | 40 | /// Clear out received data. 41 | void clear() { 42 | received.clear(); 43 | } 44 | 45 | /// Server closes the connection to client. 46 | void close() { 47 | log.add(new Packet(serverClosed, [])); 48 | _isClosed = true; 49 | _isDestroyed = true; 50 | mocket.close(); 51 | } 52 | 53 | Completer _waitForClient; 54 | 55 | /// Wait for the next packet to arrive from the client. 56 | Future waitForClient() { 57 | if (_waitForClient == null) _waitForClient = new Completer(); 58 | return _waitForClient.future; 59 | } 60 | 61 | /// Send data over the socket from the mock server to the client listening 62 | /// on the socket. 63 | void sendToClient(List data) { 64 | log.add(new Packet(toClient, data)); 65 | mocket._controller.add(data); 66 | } 67 | 68 | void socketException(String msg) { 69 | log.add(new Packet(socketError, [])); 70 | mocket._controller.addError(new SocketException(msg)); 71 | } 72 | } 73 | 74 | class MockServerImpl implements MockServer { 75 | MockServerImpl(); 76 | 77 | Future connect() => 78 | ConnectionImpl.connect('postgres://testdb:password@localhost:5433/testdb', 79 | mockSocketConnect: (host, port) => new Future(() => _startBackend())); 80 | 81 | stop() {} 82 | 83 | final List backends = []; 84 | 85 | Mocket _startBackend() { 86 | var backend = new MockServerBackendImpl(); 87 | backends.add(backend); 88 | 89 | if (_waitForConnect != null) { 90 | _waitForConnect.complete(backend); 91 | _waitForConnect = null; 92 | } 93 | 94 | return backend.mocket; 95 | } 96 | 97 | Completer _waitForConnect; 98 | 99 | /// Wait for the next client to connect. 100 | Future waitForConnect() { 101 | if (_waitForConnect == null) _waitForConnect = new Completer(); 102 | return _waitForConnect.future; 103 | } 104 | } 105 | 106 | class Mocket extends StreamView> implements Socket { 107 | factory Mocket() => new Mocket._private(new StreamController>()); 108 | 109 | Mocket._private(StreamController> ctl) 110 | : _controller = ctl, 111 | super(ctl.stream); 112 | 113 | final StreamController> _controller; 114 | 115 | bool _isDone = false; 116 | 117 | Function onClose; 118 | Function onDestroy; 119 | Function onAdd; 120 | Function onError; 121 | 122 | Future close() { 123 | _isDone = true; 124 | onClose(); 125 | return new Future.value(); 126 | } 127 | 128 | void destroy() { 129 | _isDone = true; 130 | onDestroy(); 131 | } 132 | 133 | void add(List data) => onAdd(data); 134 | 135 | void addError(error, [StackTrace stackTrace]) => onError(error, stackTrace); 136 | 137 | Future addStream(Stream> stream) { 138 | throw new UnimplementedError(); 139 | } 140 | 141 | Future get done => new Future.value(_isDone); 142 | 143 | InternetAddress get address => throw new UnimplementedError(); 144 | get encoding => throw new UnimplementedError(); 145 | void set encoding(_encoding) => throw new UnimplementedError(); 146 | Future flush() => new Future.value(null); 147 | int get port => throw new UnimplementedError(); 148 | InternetAddress get remoteAddress => throw new UnimplementedError(); 149 | int get remotePort => throw new UnimplementedError(); 150 | bool setOption(SocketOption option, bool enabled) => 151 | throw new UnimplementedError(); 152 | void write(Object obj) => throw new UnimplementedError(); 153 | 154 | void writeAll(Iterable objects, [String separator = ""]) => 155 | throw new UnimplementedError(); 156 | void writeCharCode(int charCode) => throw new UnimplementedError(); 157 | void writeln([Object obj = ""]) => throw new UnimplementedError(); 158 | } 159 | -------------------------------------------------------------------------------- /lib/src/mock/mock_socket_server.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.mock; 2 | 3 | 4 | class MockSocketServerBackendImpl implements Backend { 5 | 6 | MockSocketServerBackendImpl(this.socket) { 7 | 8 | socket.listen((data) { 9 | received.add(data); 10 | log.add(new Packet(toServer, data)); 11 | if (_waitForClient != null) { 12 | _waitForClient.complete(); 13 | _waitForClient = null; 14 | } 15 | }) 16 | 17 | ..onDone(() { 18 | _isClosed = true; 19 | log.add(new Packet(clientClosed, [])); 20 | }) 21 | 22 | //TODO 23 | ..onError((err, [st]) { 24 | _log(err); 25 | _log(st); 26 | }); 27 | } 28 | 29 | 30 | final Socket socket; 31 | 32 | final List log = new List(); 33 | final List> received = new List>(); 34 | 35 | bool _isClosed = true; 36 | bool _isDestroyed = true; 37 | bool get isClosed => _isClosed; 38 | bool get isDestroyed => _isDestroyed; 39 | 40 | /// Clear out received data. 41 | void clear() { 42 | received.clear(); 43 | } 44 | 45 | /// Server closes the connection to client. 46 | void close() { 47 | log.add(new Packet(serverClosed, [])); 48 | _isClosed = true; 49 | _isDestroyed = true; 50 | socket.close(); 51 | } 52 | 53 | 54 | Completer _waitForClient; 55 | 56 | /// Wait for the next packet to arrive from the client. 57 | Future waitForClient() { 58 | if (_waitForClient == null) 59 | _waitForClient = new Completer(); 60 | return _waitForClient.future; 61 | } 62 | 63 | /// Send data over the socket from the mock server to the client listening 64 | /// on the socket. 65 | void sendToClient(List data) { 66 | log.add(new Packet(toClient, data)); 67 | socket.add(data); 68 | } 69 | 70 | void socketException(String msg) { 71 | throw new UnsupportedError('Only valid on MockServer, not MockSocketServer'); 72 | } 73 | } 74 | 75 | 76 | class MockSocketServerImpl implements MockServer { 77 | 78 | static Future start([int port]) { 79 | port = port == null ? 5435 : port; 80 | return ServerSocket.bind('127.0.0.1', port) 81 | .then((s) => new MockSocketServerImpl._private(s)); 82 | } 83 | 84 | MockSocketServerImpl._private(this.server) { 85 | server 86 | .listen(_handleConnect) 87 | ..onError((e) => _log(e)) 88 | ..onDone(() => _log('MockSocketServer client disconnected.')); 89 | } 90 | 91 | Future connect( 92 | {String uri, 93 | Duration connectionTimeout, 94 | pg.TypeConverter typeConverter}) => pg.connect( 95 | uri == null ? 'postgres://testdb:password@localhost:${server.port}/testdb' : uri, 96 | connectionTimeout: connectionTimeout, 97 | typeConverter: typeConverter); 98 | 99 | 100 | final ServerSocket server; 101 | final List backends = []; 102 | 103 | stop() { 104 | server.close(); 105 | } 106 | 107 | _handleConnect(Socket socket) { 108 | var backend = new MockSocketServerBackendImpl(socket); 109 | backends.add(backend); 110 | 111 | if (_waitForConnect != null) { 112 | _waitForConnect.complete(backend); 113 | _waitForConnect = null; 114 | } 115 | } 116 | 117 | Completer _waitForConnect; 118 | 119 | /// Wait for the next client to connect. 120 | Future waitForConnect() { 121 | if (_waitForConnect == null) 122 | _waitForConnect = new Completer(); 123 | return _waitForConnect.future; 124 | } 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /lib/src/pool_impl.dart: -------------------------------------------------------------------------------- 1 | library postgresql.pool.impl; 2 | 3 | import 'dart:async'; 4 | import 'dart:collection'; 5 | import 'dart:io'; 6 | import 'dart:math' as math; 7 | import 'package:postgresql/constants.dart'; 8 | import 'package:postgresql/postgresql.dart' as pg; 9 | import 'package:postgresql/src/postgresql_impl/postgresql_impl.dart' as pgi; 10 | import 'package:postgresql/pool.dart'; 11 | 12 | 13 | // I like my enums short and sweet, not long and typey. 14 | const PooledConnectionState connecting = PooledConnectionState.connecting; 15 | const PooledConnectionState available = PooledConnectionState.available; 16 | const PooledConnectionState reserved = PooledConnectionState.reserved; 17 | const PooledConnectionState testing = PooledConnectionState.testing; 18 | const PooledConnectionState inUse = PooledConnectionState.inUse; 19 | const PooledConnectionState connClosed = PooledConnectionState.closed; 20 | 21 | 22 | typedef Future ConnectionFactory( 23 | String uri, 24 | {Duration connectionTimeout, 25 | String applicationName, 26 | String timeZone, 27 | pg.TypeConverter typeConverter, 28 | String getDebugName(), 29 | Future mockSocketConnect(String host, int port)}); 30 | 31 | class ConnectionDecorator implements pg.Connection { 32 | 33 | ConnectionDecorator(this._pool, PooledConnectionImpl pconn, this._conn) 34 | : _pconn = pconn, _debugName = pconn.name; 35 | 36 | _error(fnName) => new pg.PostgresqlException( 37 | '$fnName() called on closed connection.', _debugName); 38 | 39 | bool _isReleased = false; 40 | final pg.Connection _conn; 41 | final PoolImpl _pool; 42 | final PooledConnectionImpl _pconn; 43 | final String _debugName; 44 | 45 | void close() { 46 | if (!_isReleased) _pool._releaseConnection(_pconn); 47 | _isReleased = true; 48 | } 49 | 50 | Stream query(String sql, [values]) => _isReleased 51 | ? throw _error('query') 52 | : _conn.query(sql, values); 53 | 54 | Future execute(String sql, [values]) => _isReleased 55 | ? throw _error('execute') 56 | : _conn.execute(sql, values); 57 | 58 | Future runInTransaction(Future operation(), 59 | [pg.Isolation isolation = readCommitted]) 60 | => _isReleased 61 | ? throw throw _error('runInTransaction') 62 | : _conn.runInTransaction(operation, isolation); 63 | 64 | pg.ConnectionState get state => _isReleased ? closed : _conn.state; 65 | 66 | pg.TransactionState get transactionState => _isReleased 67 | ? unknown 68 | : _conn.transactionState; 69 | 70 | @deprecated pg.TransactionState get transactionStatus 71 | => transactionState; 72 | 73 | Stream get messages => _isReleased 74 | ? new Stream.fromIterable([]) 75 | : _conn.messages; 76 | 77 | @deprecated Stream get unhandled => messages; 78 | 79 | Map get parameters => _isReleased ? {} : _conn.parameters; 80 | 81 | int get backendPid => _conn.backendPid; 82 | 83 | String get debugName => _debugName; 84 | 85 | @override 86 | String toString() => "$_pconn"; 87 | } 88 | 89 | 90 | class PooledConnectionImpl implements PooledConnection { 91 | 92 | PooledConnectionImpl(this._pool); 93 | 94 | final PoolImpl _pool; 95 | pg.Connection _connection; 96 | PooledConnectionState _state; 97 | DateTime _established; 98 | DateTime _obtained; 99 | DateTime _released; 100 | String _debugName; 101 | int _useId; 102 | bool _isLeaked = false; 103 | StackTrace _stackTrace; 104 | 105 | final Duration _random = new Duration(seconds: new math.Random().nextInt(20)); 106 | 107 | PooledConnectionState get state => _state; 108 | 109 | DateTime get established => _established; 110 | 111 | DateTime get obtained => _obtained; 112 | 113 | DateTime get released => _released; 114 | 115 | int get backendPid => _connection == null ? null : _connection.backendPid; 116 | 117 | String get debugName => _debugName; 118 | 119 | int get useId => _useId; 120 | 121 | bool get isLeaked => _isLeaked; 122 | 123 | StackTrace get stackTrace => _stackTrace; 124 | 125 | pg.ConnectionState get connectionState 126 | => _connection == null ? null : _connection.state; 127 | 128 | String get name => '${_pool.settings.poolName}:$backendPid' 129 | + (_useId == null ? '' : ':$_useId') 130 | + (_debugName == null ? '' : ':$_debugName'); 131 | 132 | String toString() => '$name:$_state:$connectionState'; 133 | } 134 | 135 | //_debug(msg) => print(msg); 136 | 137 | _debug(msg) {} 138 | 139 | class PoolImpl implements Pool { 140 | 141 | PoolImpl(PoolSettings settings, 142 | this._typeConverter, 143 | [this._connectionFactory = pgi.ConnectionImpl.connect]) 144 | : settings = settings == null ? new PoolSettings() : settings; 145 | 146 | PoolState _state = initial; 147 | PoolState get state => _state; 148 | 149 | final PoolSettings settings; 150 | final pg.TypeConverter _typeConverter; 151 | final ConnectionFactory _connectionFactory; 152 | 153 | //TODO Consider using a list instead. removeAt(0); instead of removeFirst(). 154 | // Since the list will be so small there is not performance benefit using a 155 | // queue. 156 | final Queue> _waitQueue = 157 | new Queue>(); 158 | 159 | Timer _heartbeatTimer; 160 | Future _stopFuture; 161 | 162 | final StreamController _messages = 163 | new StreamController.broadcast(); 164 | 165 | final List _connections = new List(); 166 | 167 | List _connectionsView; 168 | 169 | List get connections { 170 | if (_connectionsView == null) 171 | _connectionsView = new UnmodifiableListView(_connections); 172 | return _connectionsView; 173 | } 174 | 175 | int get waitQueueLength => _waitQueue.length; 176 | 177 | Stream get messages => _messages.stream; 178 | 179 | Future start() async { 180 | _debug('start'); 181 | //TODO consider allowing moving from state stopped to starting. 182 | //Need to carefully clear out all state. 183 | if (_state != initial) 184 | throw new pg.PostgresqlException( 185 | 'Cannot start connection pool while in state: $_state.', null); 186 | 187 | var stopwatch = new Stopwatch()..start(); 188 | 189 | var onTimeout = () { 190 | _state = startFailed; 191 | throw new pg.PostgresqlException( 192 | 'Connection pool start timed out with: ' 193 | '${settings.startTimeout}).', null); 194 | }; 195 | 196 | _state = starting; 197 | 198 | // Start connections in parallel. 199 | var futures = new Iterable.generate(settings.minConnections, 200 | (i) => _establishConnection()); 201 | 202 | await Future.wait(futures) 203 | .timeout(settings.startTimeout, onTimeout: onTimeout); 204 | 205 | // If something bad happened and there are not enough connecitons. 206 | while (_connections.length < settings.minConnections) { 207 | await _establishConnection() 208 | .timeout(settings.startTimeout - stopwatch.elapsed, onTimeout: onTimeout); 209 | } 210 | 211 | _heartbeatTimer = 212 | new Timer.periodic(new Duration(seconds: 1), (_) => _heartbeat()); 213 | 214 | _state = running; 215 | } 216 | 217 | Future _establishConnection() async { 218 | _debug('Establish connection.'); 219 | 220 | // Do nothing if called while shutting down. 221 | if (!(_state == running || _state == PoolState.starting)) 222 | return new Future.value(); 223 | 224 | // This shouldn't be able to happen - but is here for robustness. 225 | if (_connections.length >= settings.maxConnections) 226 | return new Future.value(); 227 | 228 | var pconn = new PooledConnectionImpl(this); 229 | pconn._state = connecting; 230 | _connections.add(pconn); 231 | 232 | var conn = await _connectionFactory( 233 | settings.databaseUri, 234 | connectionTimeout: settings.establishTimeout, 235 | applicationName: settings.applicationName, 236 | timeZone: settings.timeZone, 237 | typeConverter: _typeConverter, 238 | getDebugName: () => pconn.name); 239 | 240 | // Pass this connection's messages through to the pool messages stream. 241 | conn.messages.listen((msg) => _messages.add(msg), 242 | onError: (msg) => _messages.addError(msg)); 243 | 244 | pconn._connection = conn; 245 | pconn._established = new DateTime.now(); 246 | pconn._state = available; 247 | 248 | _debug('Established connection. ${pconn.name}'); 249 | } 250 | 251 | void _heartbeat() { 252 | if (_state != running) return; 253 | 254 | for (var pconn in new List.from(_connections)) { 255 | _checkIfLeaked(pconn); 256 | _checkIdleTimeout(pconn); 257 | 258 | // This shouldn't be necessary, but should help fault tolerance. 259 | _processWaitQueue(); 260 | } 261 | 262 | _checkIfAllConnectionsLeaked(); 263 | } 264 | 265 | _checkIdleTimeout(PooledConnectionImpl pconn) { 266 | if (_connections.length > settings.minConnections) { 267 | if (pconn._state == available 268 | && pconn._released != null 269 | && _isExpired(pconn._released, settings.idleTimeout)) { 270 | _debug('Idle connection ${pconn.name}.'); 271 | _destroyConnection(pconn); 272 | } 273 | } 274 | } 275 | 276 | _checkIfLeaked(PooledConnectionImpl pconn) { 277 | if (settings.leakDetectionThreshold != null 278 | && !pconn._isLeaked 279 | && pconn._state != available 280 | && pconn._obtained != null 281 | && _isExpired(pconn._obtained, settings.leakDetectionThreshold)) { 282 | pconn._isLeaked = true; 283 | _messages.add(new pg.ClientMessage( 284 | severity: 'WARNING', 285 | connectionName: pconn.name, 286 | message: 'Leak detected. ' 287 | 'state: ${pconn._connection.state} ' 288 | 'transactionState: ${pconn._connection.transactionState} ' 289 | 'debugId: ${pconn.debugName}' 290 | 'stacktrace: ${pconn._stackTrace}')); 291 | } 292 | } 293 | 294 | int get _leakedConnections => 295 | _connections.where((c) => c._isLeaked).length; 296 | 297 | /// If all connections are in leaked state, then destroy them all, and 298 | /// restart the minimum required number of connections. 299 | _checkIfAllConnectionsLeaked() { 300 | if (settings.restartIfAllConnectionsLeaked 301 | && _leakedConnections >= settings.maxConnections) { 302 | 303 | _messages.add(new pg.ClientMessage( 304 | severity: 'WARNING', 305 | message: '${settings.poolName} is full of leaked connections. ' 306 | 'These will be closed and new connections started.')); 307 | 308 | // Forcefully close leaked connections. 309 | for (var pconn in new List.from(_connections)) { 310 | _destroyConnection(pconn); 311 | } 312 | 313 | // Start new connections in parallel. 314 | for (int i = 0; i < settings.minConnections; i++) { 315 | _establishConnection(); 316 | } 317 | } 318 | } 319 | 320 | // Used to generate unique ids (well... unique for this isolate at least). 321 | static int _sequence = 1; 322 | 323 | Future connect({String debugName}) async { 324 | _debug('Connect.'); 325 | 326 | if (_state != running) 327 | throw new pg.PostgresqlException( 328 | 'Connect called while pool is not running.', null); 329 | 330 | StackTrace stackTrace = null; 331 | if (settings.leakDetectionThreshold != null) { 332 | // Store the current stack trace for connection leak debugging. 333 | try { 334 | throw "Generate stacktrace."; 335 | } catch (ex, st) { 336 | stackTrace = st; 337 | } 338 | } 339 | 340 | var pconn = await _connect(settings.connectionTimeout); 341 | 342 | assert((settings.testConnections && pconn._state == testing) 343 | || (!settings.testConnections && pconn._state == reserved)); 344 | assert(pconn._connection.state == idle); 345 | assert(pconn._connection.transactionState == none); 346 | 347 | pconn.._state = inUse 348 | .._obtained = new DateTime.now() 349 | .._useId = _sequence++ 350 | .._debugName = debugName 351 | .._stackTrace = stackTrace; 352 | 353 | _debug('Connected. ${pconn.name} ${pconn._connection}'); 354 | 355 | return new ConnectionDecorator(this, pconn, pconn._connection); 356 | } 357 | 358 | Future _connect(Duration timeout) async { 359 | 360 | if (state == stopping || state == stopped) 361 | throw new pg.PostgresqlException( 362 | 'Connect failed as pool is stopping.', null); 363 | 364 | var stopwatch = new Stopwatch()..start(); 365 | 366 | var pconn = _getFirstAvailable(); 367 | 368 | timeoutException() => new pg.PostgresqlException( 369 | 'Obtaining connection from pool exceeded timeout: ' 370 | '${settings.connectionTimeout}', 371 | pconn == null ? null : pconn.name); 372 | 373 | // If there are currently no available connections then 374 | // add the current connection request at the end of the 375 | // wait queue. 376 | if (pconn == null) { 377 | var c = new Completer(); 378 | _waitQueue.add(c); 379 | try { 380 | _processWaitQueue(); 381 | pconn = await c.future.timeout(timeout, onTimeout: () => throw timeoutException()); 382 | } finally { 383 | _waitQueue.remove(c); 384 | } 385 | assert(pconn.state == reserved); 386 | } 387 | 388 | if (!settings.testConnections) { 389 | pconn._state = reserved; 390 | return pconn; 391 | } 392 | 393 | pconn._state = testing; 394 | 395 | if (await _testConnection(pconn, timeout - stopwatch.elapsed, () => throw timeoutException())) 396 | return pconn; 397 | 398 | if (timeout > stopwatch.elapsed) { 399 | throw timeoutException(); 400 | } else { 401 | _destroyConnection(pconn); 402 | // Get another connection out of the pool and test again. 403 | return _connect(timeout - stopwatch.elapsed); 404 | } 405 | } 406 | 407 | List _getAvailable() 408 | => _connections.where((c) => c._state == available).toList(); 409 | 410 | PooledConnectionImpl _getFirstAvailable() 411 | => _connections.firstWhere((c) => c._state == available, orElse: () => null); 412 | 413 | /// If connections are available, return them to waiting clients. 414 | void _processWaitQueue() { 415 | 416 | if (_state != running) return; 417 | 418 | if (_waitQueue.isEmpty) return; 419 | 420 | //FIXME make sure this happens in the correct order so it is fair to the 421 | // order which connect was called, and that connections are reused, and 422 | // others left idle so that the pool can shrink. 423 | var pconns = _getAvailable(); 424 | while(_waitQueue.isNotEmpty && pconns.isNotEmpty) { 425 | var completer = _waitQueue.removeFirst(); 426 | var pconn = pconns.removeLast(); 427 | pconn._state = reserved; 428 | completer.complete(pconn); 429 | } 430 | 431 | // If required start more connection. 432 | if (!_establishing) { //once at a time 433 | final int count = math.min(_waitQueue.length, 434 | settings.maxConnections - _connections.length); 435 | if (count > 0) { 436 | _establishing = true; 437 | new Future.sync(() { 438 | final List ops = new List(count); 439 | for (int i = 0; i < count; i++) { 440 | ops[i] = _establishConnection(); 441 | } 442 | return Future.wait(ops); 443 | }) 444 | .whenComplete(() { 445 | _establishing = false; 446 | 447 | _processWaitQueue(); //do again; there might be more requests 448 | }); 449 | } 450 | } 451 | } 452 | bool _establishing = false; 453 | 454 | /// Perfom a query to check the state of the connection. 455 | Future _testConnection( 456 | PooledConnectionImpl pconn, 457 | Duration timeout, 458 | Function onTimeout) async { 459 | bool ok; 460 | try { 461 | var row = await pconn._connection.query('select true') 462 | .single.timeout(timeout); 463 | ok = row[0]; 464 | } on Exception catch (ex) { //TODO Do I really want to log warnings when the connection timeout fails. 465 | ok = false; 466 | // Don't log connection test failures during shutdown. 467 | if (state != stopping && state != stopped) { 468 | var msg = ex is TimeoutException 469 | ? 'Connection test timed out.' 470 | : 'Connection test failed.'; 471 | _messages.add(new pg.ClientMessage( 472 | severity: 'WARNING', 473 | connectionName: pconn.name, 474 | message: msg, 475 | exception: ex)); 476 | } 477 | } 478 | return ok; 479 | } 480 | 481 | _releaseConnection(PooledConnectionImpl pconn) { 482 | _debug('Release ${pconn.name}'); 483 | 484 | if (state == stopping || state == stopped) { 485 | _destroyConnection(pconn); 486 | return; 487 | } 488 | 489 | assert(pconn._pool == this); 490 | assert(_connections.contains(pconn)); 491 | assert(pconn.state == inUse); 492 | 493 | pg.Connection conn = pconn._connection; 494 | 495 | // If connection still in transaction or busy with query then destroy. 496 | // Note this means connections which are returned with an un-committed 497 | // transaction, the entire connection will be destroyed and re-established. 498 | // While it would be possible to write code which would send a rollback 499 | // command, this is simpler and probably nearly as fast (not that this 500 | // is likely to become a bottleneck anyway). 501 | if (conn.state != idle || conn.transactionState != none) { 502 | _messages.add(new pg.ClientMessage( 503 | severity: 'WARNING', 504 | connectionName: pconn.name, 505 | message: 'Connection returned in bad state. Removing from pool. ' 506 | 'state: ${conn.state} ' 507 | 'transactionState: ${conn.transactionState}.')); 508 | 509 | _destroyConnection(pconn); 510 | _establishConnection(); 511 | 512 | // If connection older than lifetime setting then destroy. 513 | // A random number of seconds 0-20 is added, so that all connections don't 514 | // expire at exactly the same moment. 515 | } else if (settings.maxLifetime != null 516 | && _isExpired(pconn._established, settings.maxLifetime + pconn._random)) { 517 | _destroyConnection(pconn); 518 | _establishConnection(); 519 | 520 | } else { 521 | pconn._released = new DateTime.now(); 522 | pconn._state = available; 523 | _processWaitQueue(); 524 | } 525 | } 526 | 527 | bool _isExpired(DateTime time, Duration timeout) 528 | => new DateTime.now().difference(time) > timeout; 529 | 530 | _destroyConnection(PooledConnectionImpl pconn) { 531 | _debug('Destroy connection. ${pconn.name}'); 532 | if (pconn._connection != null) pconn._connection.close(); 533 | pconn._state = connClosed; 534 | _connections.remove(pconn); 535 | } 536 | 537 | /// Depreciated. Use [stop]() instead. 538 | @deprecated void destroy() { stop(); } 539 | 540 | Future stop() { 541 | _debug('Stop'); 542 | 543 | if (state == stopped || state == initial) return null; 544 | 545 | if (_stopFuture == null) 546 | _stopFuture = _stop(); 547 | else 548 | assert(state == stopping); 549 | 550 | return _stopFuture; 551 | } 552 | 553 | Future _stop() async { 554 | 555 | _state = stopping; 556 | 557 | if (_heartbeatTimer != null) _heartbeatTimer.cancel(); 558 | 559 | // Send error messages to connections in wait queue. 560 | _waitQueue.forEach((completer) => 561 | completer.completeError(new pg.PostgresqlException( 562 | 'Connection pool is stopping.', null))); 563 | _waitQueue.clear(); 564 | 565 | 566 | // Close connections as they are returned to the pool. 567 | // If stop timeout is reached then close connections even if still in use. 568 | 569 | var stopwatch = new Stopwatch()..start(); 570 | while (_connections.isNotEmpty) { 571 | _getAvailable().forEach(_destroyConnection); 572 | 573 | await new Future.delayed(new Duration(milliseconds: 100), () => null); 574 | 575 | if (stopwatch.elapsed > settings.stopTimeout ) { 576 | _messages.add(new pg.ClientMessage( 577 | severity: 'WARNING', 578 | message: 'Exceeded timeout while stopping pool, ' 579 | 'closing in use connections.')); 580 | // _destroyConnection modifies this list, so need to make a copy. 581 | new List.from(_connections).forEach(_destroyConnection); 582 | } 583 | } 584 | _state = stopped; 585 | 586 | _debug('Stopped'); 587 | } 588 | 589 | } 590 | 591 | 592 | 593 | 594 | 595 | 596 | -------------------------------------------------------------------------------- /lib/src/pool_settings_impl.dart: -------------------------------------------------------------------------------- 1 | library postgresql.pool.pool_settings_impl; 2 | 3 | import 'dart:convert'; 4 | import 'package:postgresql/pool.dart'; 5 | import 'package:postgresql/postgresql.dart' as pg; 6 | import 'package:postgresql/src/duration_format.dart'; 7 | 8 | final PoolSettingsImpl _default = new PoolSettingsImpl(); 9 | 10 | class PoolSettingsImpl implements PoolSettings { 11 | 12 | PoolSettingsImpl({ 13 | this.databaseUri, 14 | String poolName, 15 | this.minConnections: 5, 16 | this.maxConnections: 10, 17 | this.startTimeout: const Duration(seconds: 30), 18 | this.stopTimeout: const Duration(seconds: 30), 19 | this.establishTimeout: const Duration(seconds: 30), 20 | this.connectionTimeout: const Duration(seconds: 30), 21 | this.idleTimeout: const Duration(minutes: 10), 22 | this.maxLifetime: const Duration(minutes: 30), 23 | this.leakDetectionThreshold: null, // Disabled by default. 24 | this.testConnections: false, 25 | this.restartIfAllConnectionsLeaked: false, 26 | this.applicationName, 27 | this.timeZone}) 28 | : poolName = poolName != null ? poolName : 'pgpool${_sequence++}'; 29 | 30 | 31 | // Ugly work around for passing defaults from Pool constructor. 32 | factory PoolSettingsImpl.withDefaults({ 33 | String databaseUri, 34 | String poolName, 35 | int minConnections, 36 | int maxConnections, 37 | Duration startTimeout, 38 | Duration stopTimeout, 39 | Duration establishTimeout, 40 | Duration connectionTimeout, 41 | Duration idleTimeout, 42 | Duration maxLifetime, 43 | Duration leakDetectionThreshold, 44 | bool testConnections, 45 | bool restartIfAllConnectionsLeaked, 46 | String applicationName, 47 | String timeZone}) { 48 | 49 | return new PoolSettingsImpl( 50 | databaseUri: databaseUri, 51 | poolName: poolName, 52 | minConnections: minConnections == null ? _default.minConnections : minConnections, 53 | maxConnections: maxConnections == null ? _default.maxConnections : maxConnections, 54 | startTimeout: startTimeout == null ? _default.startTimeout : startTimeout, 55 | stopTimeout: stopTimeout == null ? _default.stopTimeout : stopTimeout, 56 | establishTimeout: establishTimeout == null ? _default.establishTimeout : establishTimeout, 57 | connectionTimeout: connectionTimeout == null ? _default.connectionTimeout : connectionTimeout, 58 | idleTimeout: idleTimeout == null ? _default.idleTimeout : idleTimeout, 59 | maxLifetime: maxLifetime == null ? _default.maxLifetime : maxLifetime, 60 | leakDetectionThreshold: leakDetectionThreshold == null ? _default.leakDetectionThreshold : leakDetectionThreshold, 61 | testConnections: testConnections == null ? _default.testConnections : testConnections, 62 | restartIfAllConnectionsLeaked: restartIfAllConnectionsLeaked == null ? _default.restartIfAllConnectionsLeaked : restartIfAllConnectionsLeaked, 63 | applicationName: applicationName, 64 | timeZone: timeZone); 65 | } 66 | 67 | // Ids will be unique for this isolate. 68 | static int _sequence = 0; 69 | 70 | 71 | final String databaseUri; 72 | final String poolName; 73 | final int minConnections; 74 | final int maxConnections; 75 | final Duration startTimeout; 76 | final Duration stopTimeout; 77 | final Duration establishTimeout; 78 | final Duration connectionTimeout; 79 | final Duration idleTimeout; 80 | final Duration maxLifetime; 81 | final Duration leakDetectionThreshold; 82 | final bool testConnections; 83 | final bool restartIfAllConnectionsLeaked; 84 | final String applicationName; 85 | final String timeZone; 86 | 87 | static final DurationFormat _durationFmt = new DurationFormat(); 88 | 89 | factory PoolSettingsImpl.fromMap(Map map) { 90 | 91 | var uri = map['databaseUri']; 92 | 93 | if (uri == null) { 94 | try { 95 | uri = new pg.Settings.fromMap(map).toUri(); 96 | } on pg.PostgresqlException { 97 | } 98 | } 99 | 100 | fail(String msg) => throw new pg.PostgresqlException( 101 | 'Pool setting $msg', null); 102 | 103 | bool getBool(String field) { 104 | var value = map[field]; 105 | if (value == null) return null; 106 | if (value is! bool) 107 | fail('$field requires boolean value was: ${value.runtimeType}.'); 108 | return value; 109 | } 110 | 111 | int getInt(String field) { 112 | var value = map[field]; 113 | if (value == null) return null; 114 | if (value is! int) 115 | fail('$field requires int value was: ${value.runtimeType}.'); 116 | return value; 117 | } 118 | 119 | String getString(String field) { 120 | var value = map[field]; 121 | if (value == null) return null; 122 | if (value is! String) 123 | fail('$field requires string value was: ${value.runtimeType}.'); 124 | return value; 125 | } 126 | 127 | Duration getDuration(String field) { 128 | var value = map[field]; 129 | if (value == null) return null; 130 | fail2([String _]) => fail('$field is not a duration string: "$value". Use this format: "120s".'); 131 | if (value is! String) fail2(); 132 | return _durationFmt.parse(value, onError: fail2); 133 | } 134 | 135 | var settings = new PoolSettingsImpl.withDefaults( 136 | databaseUri: uri, 137 | poolName: getString('poolName'), 138 | minConnections: getInt('minConnections'), 139 | maxConnections: getInt('maxConnections'), 140 | startTimeout: getDuration('startTimeout'), 141 | stopTimeout: getDuration('stopTimeout'), 142 | establishTimeout: getDuration('establishTimeout'), 143 | connectionTimeout: getDuration('connectionTimeout'), 144 | idleTimeout: getDuration('idleTimeout'), 145 | maxLifetime: getDuration('maxLifetime'), 146 | leakDetectionThreshold: getDuration('leakDetectionThreshold'), 147 | testConnections: getBool('testConnections'), 148 | restartIfAllConnectionsLeaked: getBool('restartIfAllConnectionsLeaked'), 149 | applicationName: getString('applicationName'), 150 | timeZone: getString('timeZone')); 151 | 152 | return settings; 153 | } 154 | 155 | Map toMap() { 156 | String fmt(Duration d) => d == null ? null : _durationFmt.format(d); 157 | Map m = {'databaseUri': databaseUri}; 158 | if (poolName != null) m['poolName'] = poolName; 159 | if (minConnections != null) m['minConnections'] = minConnections; 160 | if (maxConnections != null) m['maxConnections'] = maxConnections; 161 | if (startTimeout != null) m['startTimeout'] = fmt(startTimeout); 162 | if (stopTimeout != null) m['stopTimeout'] = fmt(stopTimeout); 163 | if (establishTimeout != null) m['establishTimeout'] = fmt(establishTimeout); 164 | if (connectionTimeout != null) 165 | m['connectionTimeout'] = fmt(connectionTimeout); 166 | if (idleTimeout != null) m['idleTimeout'] = fmt(idleTimeout); 167 | if (maxLifetime != null) m['maxLifetime'] = fmt(maxLifetime); 168 | if (leakDetectionThreshold != null) 169 | m['leakDetectionThreshold'] = fmt(leakDetectionThreshold); 170 | if (testConnections != null) m['testConnections'] = testConnections; 171 | if (restartIfAllConnectionsLeaked != null) 172 | m['restartIfAllConnectionsLeaked'] = restartIfAllConnectionsLeaked; 173 | if (applicationName != null) m['applicationName'] = applicationName; 174 | if (timeZone != null) m['timeZone'] = timeZone; 175 | 176 | return m; 177 | } 178 | 179 | Map toJson() => toMap(); 180 | 181 | toString() { 182 | 183 | // Stip out password - better not to accidentally log it. 184 | var s = new pg.Settings.fromUri(databaseUri); 185 | var m = s.toMap(); 186 | m['password'] = 'xxxx'; 187 | var uri = new pg.Settings.fromMap(m).toUri(); 188 | 189 | var map = toMap(); 190 | map['databaseUri'] = uri; 191 | var settings = new PoolSettings.fromMap(map); 192 | var json = JSON.encode(settings); 193 | 194 | return 'PoolSettings $json'; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/connection.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.impl; 2 | 3 | class ConnectionImpl implements Connection { 4 | 5 | ConnectionImpl._private( 6 | this._socket, 7 | Settings settings, 8 | this._applicationName, 9 | this._timeZone, 10 | TypeConverter typeConverter, 11 | String getDebugName()) 12 | : _userName = settings.user, 13 | _passwordHash = _md5s(settings.password + settings.user), 14 | _databaseName = settings.database, 15 | _typeConverter = typeConverter == null 16 | ? new TypeConverter() 17 | : typeConverter, 18 | _getDebugName = getDebugName, 19 | _buffer = new Buffer((msg) => new PostgresqlException(msg, getDebugName())); 20 | 21 | ConnectionState get state => _state; 22 | ConnectionState _state = notConnected; 23 | 24 | TransactionState _transactionState = unknown; 25 | TransactionState get transactionState => _transactionState; 26 | 27 | @deprecated TransactionState get transactionStatus => _transactionState; 28 | 29 | final String _databaseName; 30 | final String _userName; 31 | final String _passwordHash; 32 | final String _applicationName; 33 | final String _timeZone; 34 | final TypeConverter _typeConverter; 35 | final Socket _socket; 36 | final Buffer _buffer; 37 | bool _hasConnected = false; 38 | final Completer _connected = new Completer(); 39 | final Queue<_Query> _sendQueryQueue = new Queue<_Query>(); 40 | _Query _query; 41 | int _msgType; 42 | int _msgLength; 43 | int _secretKey; 44 | bool _isUtcTimeZone = false; 45 | 46 | int _backendPid; 47 | final _getDebugName; 48 | 49 | int get backendPid => _backendPid; 50 | 51 | String get debugName => _getDebugName(); 52 | 53 | String toString() => '$debugName:$_backendPid'; 54 | 55 | final Map _parameters = new Map(); 56 | 57 | Map _parametersView; 58 | 59 | Map get parameters { 60 | if (_parametersView == null) 61 | _parametersView = new UnmodifiableMapView(_parameters); 62 | return _parametersView; 63 | } 64 | 65 | Stream get messages => _messages.stream as Stream; 66 | 67 | @deprecated Stream get unhandled => messages; 68 | 69 | final StreamController _messages = new StreamController.broadcast(); 70 | 71 | static Future connect( 72 | String uri, 73 | {Duration connectionTimeout, 74 | String applicationName, 75 | String timeZone, 76 | TypeConverter typeConverter, 77 | String getDebugName(), 78 | Future mockSocketConnect(String host, int port)}) { 79 | 80 | return new Future.sync(() { 81 | 82 | var settings = new Settings.fromUri(uri); 83 | 84 | //FIXME Currently this timeout doesn't cancel the socket connection 85 | // process. 86 | // There is a bug open about adding a real socket connect timeout 87 | // parameter to Socket.connect() if this happens then start using it. 88 | // http://code.google.com/p/dart/issues/detail?id=19120 89 | if (connectionTimeout == null) 90 | connectionTimeout = new Duration(seconds: 180); 91 | 92 | getDebugName = getDebugName == null ? () => 'pgconn' : getDebugName; 93 | 94 | var onTimeout = () => throw new PostgresqlException( 95 | 'Postgresql connection timed out. Timeout: $connectionTimeout.', 96 | getDebugName()); 97 | 98 | var connectFunc = mockSocketConnect == null 99 | ? Socket.connect 100 | : mockSocketConnect; 101 | 102 | Future future = connectFunc(settings.host, settings.port) 103 | .timeout(connectionTimeout, onTimeout: onTimeout); 104 | 105 | if (settings.requireSsl) future = _connectSsl(future); 106 | 107 | return future.timeout(connectionTimeout, onTimeout: onTimeout).then((socket) { 108 | 109 | var conn = new ConnectionImpl._private(socket, settings, 110 | applicationName, timeZone, typeConverter, getDebugName); 111 | 112 | socket.listen(conn._readData, 113 | onError: conn._handleSocketError, 114 | onDone: conn._handleSocketClosed); 115 | 116 | conn._state = socketConnected; 117 | conn._sendStartupMessage(); 118 | return conn._connected.future; 119 | }); 120 | }); 121 | } 122 | 123 | static String _md5s(String s) { 124 | var digest = md5.convert(s.codeUnits.toList()); 125 | return hex.encode(digest.bytes); 126 | } 127 | 128 | //TODO yuck - this needs a rewrite. 129 | static Future _connectSsl(Future future) { 130 | 131 | var completer = new Completer(); 132 | 133 | future.then((socket) { 134 | 135 | socket.listen((data) { 136 | if (data == null || data[0] != _S) { 137 | socket.destroy(); 138 | completer.completeError( 139 | new PostgresqlException( 140 | 'This postgresql server is not configured to support SSL ' 141 | 'connections.', null)); //FIXME ideally pass the connection pool name through to this exception. 142 | } else { 143 | // TODO add option to only allow valid certs. 144 | // Note libpq also defaults to ignoring bad certificates, so this is 145 | // expected behaviour. 146 | // TODO consider adding a warning if certificate is invalid so that it 147 | // is at least logged. 148 | new Future.sync(() => SecureSocket.secure(socket, onBadCertificate: (cert) => true)) 149 | .then((s) => completer.complete(s)) 150 | .catchError((e) => completer.completeError(e)); 151 | } 152 | }); 153 | 154 | // Write header, and SSL magic number. 155 | socket.add([0, 0, 0, 8, 4, 210, 22, 47]); 156 | 157 | }) 158 | .catchError((e) { 159 | completer.completeError(e); 160 | }); 161 | 162 | return completer.future; 163 | } 164 | 165 | void _sendStartupMessage() { 166 | if (_state != socketConnected) 167 | throw new PostgresqlException( 168 | 'Invalid state during startup.', _getDebugName()); 169 | 170 | var msg = new MessageBuffer(); 171 | msg.addInt32(0); // Length padding. 172 | msg.addInt32(_PROTOCOL_VERSION); 173 | msg.addUtf8String('user'); 174 | msg.addUtf8String(_userName); 175 | msg.addUtf8String('database'); 176 | msg.addUtf8String(_databaseName); 177 | msg.addUtf8String('client_encoding'); 178 | msg.addUtf8String('UTF8'); 179 | if (_timeZone != null) { 180 | msg.addUtf8String('TimeZone'); 181 | msg.addUtf8String(_timeZone); 182 | } 183 | if (_applicationName != null) { 184 | msg.addUtf8String('application_name'); 185 | msg.addUtf8String(_applicationName); 186 | } 187 | msg.addByte(0); 188 | msg.setLength(startup: true); 189 | 190 | _socket.add(msg.buffer); 191 | 192 | _state = authenticating; 193 | } 194 | 195 | void _readAuthenticationRequest(int msgType, int length) { 196 | assert(_buffer.bytesAvailable >= length); 197 | 198 | if (_state != authenticating) 199 | throw new PostgresqlException( 200 | 'Invalid connection state while authenticating.', _getDebugName()); 201 | 202 | int authType = _buffer.readInt32(); 203 | 204 | if (authType == _AUTH_TYPE_OK) { 205 | _state = authenticated; 206 | return; 207 | } 208 | 209 | // Only MD5 authentication is supported. 210 | if (authType != _AUTH_TYPE_MD5) { 211 | throw new PostgresqlException('Unsupported or unknown authentication ' 212 | 'type: ${_authTypeAsString(authType)}, only MD5 authentication is ' 213 | 'supported.', _getDebugName()); 214 | } 215 | 216 | var bytes = _buffer.readBytes(4); 217 | var salt = new String.fromCharCodes(bytes); 218 | var md5 = 'md5' + _md5s('${_passwordHash}$salt'); 219 | 220 | // Build message. 221 | var msg = new MessageBuffer(); 222 | msg.addByte(_MSG_PASSWORD); 223 | msg.addInt32(0); 224 | msg.addUtf8String(md5); 225 | msg.setLength(); 226 | 227 | _socket.add(msg.buffer); 228 | } 229 | 230 | void _readReadyForQuery(int msgType, int length) { 231 | assert(_buffer.bytesAvailable >= length); 232 | 233 | int c = _buffer.readByte(); 234 | 235 | if (c == _I || c == _T || c == _E) { 236 | 237 | if (c == _I) 238 | _transactionState = none; 239 | else if (c == _T) 240 | _transactionState = begun; 241 | else if (c == _E) 242 | _transactionState = error; 243 | 244 | var was = _state; 245 | 246 | _state = idle; 247 | 248 | if (_query != null) { 249 | _query.close(); 250 | _query = null; 251 | } 252 | 253 | if (was == authenticated) { 254 | _hasConnected = true; 255 | _connected.complete(this); 256 | } 257 | 258 | new Future(() => _processSendQueryQueue()); 259 | 260 | } else { 261 | _destroy(); 262 | throw new PostgresqlException('Unknown ReadyForQuery transaction status: ' 263 | '${_itoa(c)}.', _getDebugName()); 264 | } 265 | } 266 | 267 | void _handleSocketError(error, {bool closed: false}) { 268 | 269 | if (_state == closed) { 270 | _messages.add(new ClientMessageImpl( 271 | isError: false, 272 | severity: 'WARNING', 273 | message: 'Socket error after socket closed.', 274 | connectionName: _getDebugName(), 275 | exception: error)); 276 | _destroy(); 277 | return; 278 | } 279 | 280 | _destroy(); 281 | 282 | var msg = closed ? 'Socket closed unexpectedly.' : 'Socket error.'; 283 | 284 | if (!_hasConnected) { 285 | _connected.completeError(new PostgresqlException(msg, _getDebugName(), 286 | exception: error)); 287 | } else if (_query != null) { 288 | _query.addError(new PostgresqlException(msg, _getDebugName(), 289 | exception: error)); 290 | } else { 291 | _messages.add(new ClientMessage( 292 | isError: true, connectionName: _getDebugName(), severity: 'ERROR', 293 | message: msg, exception: error)); 294 | } 295 | } 296 | 297 | void _handleSocketClosed() { 298 | if (_state != closed) { 299 | _handleSocketError(null, closed: true); 300 | } 301 | } 302 | 303 | void _readData(List data) { 304 | 305 | try { 306 | 307 | if (_state == closed) 308 | return; 309 | 310 | _buffer.append(data); 311 | 312 | // Handle resuming after storing message type and length. 313 | if (_msgType != null) { 314 | if (_msgLength > _buffer.bytesAvailable) 315 | return; // Wait for entire message to be in buffer. 316 | 317 | _readMessage(_msgType, _msgLength); 318 | 319 | _msgType = null; 320 | _msgLength = null; 321 | } 322 | 323 | // Main message loop. 324 | while (_state != closed) { 325 | 326 | if (_buffer.bytesAvailable < 5) 327 | return; // Wait for more data. 328 | 329 | // Message length is the message length excluding the message type code, but 330 | // including the 4 bytes for the length fields. Only the length of the body 331 | // is passed to each of the message handlers. 332 | int msgType = _buffer.readByte(); 333 | int length = _buffer.readInt32() - 4; 334 | 335 | if (!_checkMessageLength(msgType, length + 4)) { 336 | throw new PostgresqlException('Lost message sync.', _getDebugName()); 337 | } 338 | 339 | if (length > _buffer.bytesAvailable) { 340 | // Wait for entire message to be in buffer. 341 | // Store type, and length for when more data becomes available. 342 | _msgType = msgType; 343 | _msgLength = length; 344 | return; 345 | } 346 | 347 | _readMessage(msgType, length); 348 | } 349 | 350 | } on Exception { 351 | _destroy(); 352 | rethrow; 353 | } 354 | } 355 | 356 | bool _checkMessageLength(int msgType, int msgLength) { 357 | 358 | if (_state == authenticating) { 359 | if (msgLength < 8) return false; 360 | if (msgType == _MSG_AUTH_REQUEST && msgLength > 2000) return false; 361 | if (msgType == _MSG_ERROR_RESPONSE && msgLength > 30000) return false; 362 | } else { 363 | if (msgLength < 4) return false; 364 | 365 | // These are the only messages from the server which may exceed 30,000 366 | // bytes. 367 | if (msgLength > 30000 && (msgType != _MSG_NOTICE_RESPONSE 368 | && msgType != _MSG_ERROR_RESPONSE 369 | && msgType != _MSG_COPY_DATA 370 | && msgType != _MSG_ROW_DESCRIPTION 371 | && msgType != _MSG_DATA_ROW 372 | && msgType != _MSG_FUNCTION_CALL_RESPONSE 373 | && msgType != _MSG_NOTIFICATION_RESPONSE)) { 374 | return false; 375 | } 376 | } 377 | return true; 378 | } 379 | 380 | void _readMessage(int msgType, int length) { 381 | 382 | int pos = _buffer.bytesRead; 383 | 384 | switch (msgType) { 385 | 386 | case _MSG_AUTH_REQUEST: _readAuthenticationRequest(msgType, length); break; 387 | case _MSG_READY_FOR_QUERY: _readReadyForQuery(msgType, length); break; 388 | 389 | case _MSG_ERROR_RESPONSE: 390 | case _MSG_NOTICE_RESPONSE: 391 | _readErrorOrNoticeResponse(msgType, length); break; 392 | 393 | case _MSG_BACKEND_KEY_DATA: _readBackendKeyData(msgType, length); break; 394 | case _MSG_PARAMETER_STATUS: _readParameterStatus(msgType, length); break; 395 | 396 | case _MSG_ROW_DESCRIPTION: _readRowDescription(msgType, length); break; 397 | case _MSG_DATA_ROW: _readDataRow(msgType, length); break; 398 | case _MSG_EMPTY_QUERY_REPONSE: assert(length == 0); break; 399 | case _MSG_COMMAND_COMPLETE: _readCommandComplete(msgType, length); break; 400 | 401 | default: 402 | throw new PostgresqlException('Unknown, or unimplemented message: ' 403 | '${UTF8.decode([msgType])}.', _getDebugName()); 404 | } 405 | 406 | if (pos + length != _buffer.bytesRead) 407 | throw new PostgresqlException('Lost message sync.', _getDebugName()); 408 | } 409 | 410 | void _readErrorOrNoticeResponse(int msgType, int length) { 411 | assert(_buffer.bytesAvailable >= length); 412 | 413 | var map = new Map(); 414 | int errorCode = _buffer.readByte(); 415 | while (errorCode != 0) { 416 | var msg = _buffer.readUtf8String(length); //TODO check length remaining. 417 | map[new String.fromCharCode(errorCode)] = msg; 418 | errorCode = _buffer.readByte(); 419 | } 420 | 421 | var msg = new ServerMessageImpl( 422 | msgType == _MSG_ERROR_RESPONSE, 423 | map, 424 | _getDebugName()); 425 | 426 | var ex = new PostgresqlException(msg.message, _getDebugName(), 427 | serverMessage: msg); 428 | 429 | if (msgType == _MSG_ERROR_RESPONSE) { 430 | if (!_hasConnected) { 431 | _state = closed; 432 | _socket.destroy(); 433 | _connected.completeError(ex); 434 | } else if (_query != null) { 435 | _query.addError(ex); 436 | } else { 437 | _messages.add(msg); 438 | } 439 | } else { 440 | _messages.add(msg); 441 | } 442 | } 443 | 444 | void _readBackendKeyData(int msgType, int length) { 445 | assert(_buffer.bytesAvailable >= length); 446 | _backendPid = _buffer.readInt32(); 447 | _secretKey = _buffer.readInt32(); 448 | } 449 | 450 | void _readParameterStatus(int msgType, int length) { 451 | assert(_buffer.bytesAvailable >= length); 452 | var name = _buffer.readUtf8String(10000); 453 | var value = _buffer.readUtf8String(10000); 454 | 455 | warn(msg) { 456 | _messages.add(new ClientMessageImpl( 457 | severity: 'WARNING', 458 | message: msg, 459 | connectionName: _getDebugName())); 460 | } 461 | 462 | _parameters[name] = value; 463 | 464 | // Cache this value so that it doesn't need to be looked up from the map. 465 | if (name == 'TimeZone') { 466 | _isUtcTimeZone = value == 'UTC'; 467 | } 468 | 469 | if (name == 'client_encoding' && value != 'UTF8') { 470 | warn('client_encoding parameter must remain as UTF8 for correct string ' 471 | 'handling. client_encoding is: "$value".'); 472 | } 473 | } 474 | 475 | Stream query(String sql, [values]) { 476 | try { 477 | if (values != null) 478 | sql = substitute(sql, values, _typeConverter.encode); 479 | var query = _enqueueQuery(sql); 480 | return query.stream as Stream; 481 | } on Exception catch (ex, st) { 482 | return new Stream.fromFuture(new Future.error(ex, st)); 483 | } 484 | } 485 | 486 | Future execute(String sql, [values]) { 487 | try { 488 | if (values != null) 489 | sql = substitute(sql, values, _typeConverter.encode); 490 | 491 | var query = _enqueueQuery(sql); 492 | return query.stream.isEmpty.then((_) => query._rowsAffected); 493 | } on Exception catch (ex, st) { 494 | return new Future.error(ex, st); 495 | } 496 | } 497 | 498 | Future runInTransaction(Future operation(), [Isolation isolation = readCommitted]) { 499 | 500 | var begin = 'begin'; 501 | if (isolation == repeatableRead) 502 | begin = 'begin; set transaction isolation level repeatable read;'; 503 | else if (isolation == serializable) 504 | begin = 'begin; set transaction isolation level serializable;'; 505 | 506 | return execute(begin) 507 | .then((_) => operation()) 508 | .then((_) => execute('commit')) 509 | .catchError((e, st) { 510 | return execute('rollback') 511 | .then((_) => new Future.error(e, st)); 512 | }); 513 | } 514 | 515 | _Query _enqueueQuery(String sql) { 516 | 517 | if (sql == null || sql == '') 518 | throw new PostgresqlException( 519 | 'SQL query is null or empty.', _getDebugName()); 520 | 521 | if (sql.contains('\u0000')) 522 | throw new PostgresqlException( 523 | 'Sql query contains a null character.', _getDebugName()); 524 | 525 | if (_state == closed) 526 | throw new PostgresqlException( 527 | 'Connection is closed, cannot execute query.', _getDebugName()); 528 | 529 | var query = new _Query(sql); 530 | _sendQueryQueue.addLast(query); 531 | 532 | new Future(() => _processSendQueryQueue()); 533 | 534 | return query; 535 | } 536 | 537 | void _processSendQueryQueue() { 538 | 539 | if (_sendQueryQueue.isEmpty) 540 | return; 541 | 542 | if (_query != null) 543 | return; 544 | 545 | if (_state == closed) 546 | return; 547 | 548 | assert(_state == idle); 549 | 550 | _query = _sendQueryQueue.removeFirst(); 551 | 552 | var msg = new MessageBuffer(); 553 | msg.addByte(_MSG_QUERY); 554 | msg.addInt32(0); // Length padding. 555 | msg.addUtf8String(_query.sql); 556 | msg.setLength(); 557 | 558 | _socket.add(msg.buffer); 559 | 560 | _state = busy; 561 | _query._state = _BUSY; 562 | _transactionState = unknown; 563 | } 564 | 565 | void _readRowDescription(int msgType, int length) { 566 | 567 | assert(_buffer.bytesAvailable >= length); 568 | 569 | _state = streaming; 570 | 571 | int count = _buffer.readInt16(); 572 | var list = new List<_Column>(count); 573 | 574 | for (int i = 0; i < count; i++) { 575 | var name = _buffer.readUtf8String(length); //TODO better maxSize. 576 | int fieldId = _buffer.readInt32(); 577 | int tableColNo = _buffer.readInt16(); 578 | int fieldType = _buffer.readInt32(); 579 | int dataSize = _buffer.readInt16(); 580 | int typeModifier = _buffer.readInt32(); 581 | int formatCode = _buffer.readInt16(); 582 | 583 | list[i] = new _Column(i, name, fieldId, tableColNo, fieldType, dataSize, typeModifier, formatCode); 584 | } 585 | 586 | _query._columnCount = count; 587 | _query._columns = new UnmodifiableListView(list); 588 | _query._commandIndex++; 589 | 590 | _query.addRowDescription(); 591 | } 592 | 593 | void _readDataRow(int msgType, int length) { 594 | 595 | assert(_buffer.bytesAvailable >= length); 596 | 597 | int columns = _buffer.readInt16(); 598 | for (var i = 0; i < columns; i++) { 599 | int size = _buffer.readInt32(); 600 | _readColumnData(i, size); 601 | } 602 | } 603 | 604 | void _readColumnData(int index, int colSize) { 605 | 606 | assert(_buffer.bytesAvailable >= colSize); 607 | 608 | if (index == 0) 609 | _query._rowData = new List(_query._columns.length); 610 | 611 | if (colSize == -1) { 612 | _query._rowData[index] = null; 613 | } else { 614 | var col = _query._columns[index]; 615 | if (col.isBinary) throw new PostgresqlException( 616 | 'Binary result set parsing is not implemented.', _getDebugName()); 617 | 618 | var str = _buffer.readUtf8StringN(colSize); 619 | 620 | var value = _typeConverter.decode(str, col.fieldType, 621 | isUtcTimeZone: _isUtcTimeZone, getConnectionName: _getDebugName); 622 | 623 | _query._rowData[index] = value; 624 | } 625 | 626 | // If last column, then return the row. 627 | if (index == _query._columnCount - 1) 628 | _query.addRow(); 629 | } 630 | 631 | void _readCommandComplete(int msgType, int length) { 632 | 633 | assert(_buffer.bytesAvailable >= length); 634 | 635 | var commandString = _buffer.readUtf8String(length); 636 | int rowsAffected = 637 | int.parse(commandString.split(' ').last, onError: (_) => null); 638 | 639 | _query._commandIndex++; 640 | _query._rowsAffected = rowsAffected; 641 | } 642 | 643 | void close() { 644 | 645 | if (_state == closed) 646 | return; 647 | 648 | _state = closed; 649 | 650 | // If a query is in progress then send an error and close the result stream. 651 | if (_query != null) { 652 | var c = _query._controller; 653 | if (c != null && !c.isClosed) { 654 | c.addError(new PostgresqlException( 655 | 'Connection closed before query could complete', _getDebugName())); 656 | c.close(); 657 | _query = null; 658 | } 659 | } 660 | 661 | Future flushing; 662 | try { 663 | var msg = new MessageBuffer(); 664 | msg.addByte(_MSG_TERMINATE); 665 | msg.addInt32(0); 666 | msg.setLength(); 667 | _socket.add(msg.buffer); 668 | flushing = _socket.flush(); 669 | } on Exception catch (e, st) { 670 | _messages.add(new ClientMessageImpl( 671 | severity: 'WARNING', 672 | message: 'Exception while closing connection. Closed without sending ' 673 | 'terminate message.', 674 | connectionName: _getDebugName(), 675 | exception: e, 676 | stackTrace: st)); 677 | } 678 | 679 | // Wait for socket flush to succeed or fail before closing the connection. 680 | flushing.whenComplete(_destroy); 681 | } 682 | 683 | void _destroy() { 684 | _state = closed; 685 | _socket.destroy(); 686 | new Future(() => _messages.close()); 687 | } 688 | 689 | } 690 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/constants.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.impl; 2 | 3 | const int _QUEUED = 1; 4 | const int _BUSY = 6; 5 | const int _STREAMING = 7; 6 | const int _DONE = 8; 7 | 8 | const int _I = 73; 9 | const int _T = 84; 10 | const int _E = 69; 11 | 12 | const int _t = 116; 13 | const int _M = 77; 14 | const int _S = 83; 15 | 16 | const int _PROTOCOL_VERSION = 196608; 17 | 18 | const int _AUTH_TYPE_MD5 = 5; 19 | const int _AUTH_TYPE_OK = 0; 20 | 21 | // Messages sent by client (Frontend). 22 | const int _MSG_STARTUP = -1; // Fake message type as StartupMessage has no type in the header. 23 | const int _MSG_PASSWORD = 112; // 'p' 24 | const int _MSG_QUERY = 81; // 'Q' 25 | const int _MSG_TERMINATE = 88; // 'X' 26 | 27 | // Message types sent by the server. 28 | const int _MSG_AUTH_REQUEST = 82; //'R'.charCodeAt(0); 29 | const int _MSG_ERROR_RESPONSE = 69; //'E'.charCodeAt(0); 30 | const int _MSG_BACKEND_KEY_DATA = 75; //'K'.charCodeAt(0); 31 | const int _MSG_PARAMETER_STATUS = 83; //'S'.charCodeAt(0); 32 | const int _MSG_NOTICE_RESPONSE = 78; //'N'.charCodeAt(0); 33 | const int _MSG_NOTIFICATION_RESPONSE = 65; //'A'.charCodeAt(0); 34 | const int _MSG_BIND = 66; //'B'.charCodeAt(0); 35 | const int _MSG_BIND_COMPLETE = 50; //'2'.charCodeAt(0); 36 | const int _MSG_CLOSE_COMPLETE = 51; //'3'.charCodeAt(0); 37 | const int _MSG_COMMAND_COMPLETE = 67; //'C'.charCodeAt(0); 38 | const int _MSG_COPY_DATA = 100; //'d'.charCodeAt(0); 39 | const int _MSG_COPY_DONE = 99; //'c'.charCodeAt(0); 40 | const int _MSG_COPY_IN_RESPONSE = 71; //'G'.charCodeAt(0); 41 | const int _MSG_COPY_OUT_RESPONSE = 72; //'H'.charCodeAt(0); 42 | const int _MSG_COPY_BOTH_RESPONSE = 87; //'W'.charCodeAt(0); 43 | const int _MSG_DATA_ROW = 68; //'D'.charCodeAt(0); 44 | const int _MSG_EMPTY_QUERY_REPONSE = 73; //'I'.charCodeAt(0); 45 | const int _MSG_FUNCTION_CALL_RESPONSE = 86; //'V'.charCodeAt(0); 46 | const int _MSG_NO_DATA = 110; //'n'.charCodeAt(0); 47 | const int _MSG_PARAMETER_DESCRIPTION = 116; //'t'.charCodeAt(0); 48 | const int _MSG_PARSE_COMPLETE = 49; //'1'.charCodeAt(0); 49 | const int _MSG_PORTAL_SUSPENDED = 115; //'s'.charCodeAt(0); 50 | const int _MSG_READY_FOR_QUERY = 90; //'Z'.charCodeAt(0); 51 | const int _MSG_ROW_DESCRIPTION = 84; //'T'.charCodeAt(0); 52 | 53 | String _itoa(int c) { 54 | try { 55 | return new String.fromCharCodes([c]); 56 | } catch (ex) { 57 | return 'Invalid'; 58 | } 59 | } 60 | 61 | String _authTypeAsString(int authType) { 62 | const unknown = 'Unknown'; 63 | const names = const ['Authentication OK', 64 | unknown, 65 | 'Kerberos v5', 66 | 'cleartext password', 67 | unknown, 68 | 'MD5 password', 69 | 'SCM credentials', 70 | 'GSSAPI', 71 | 'GSSAPI or SSPI authentication data', 72 | 'SSPI']; 73 | var type = unknown; 74 | if (authType > 0 && authType < names.length) 75 | type = names[authType]; 76 | return type; 77 | } 78 | 79 | /// Constants for postgresql datatypes 80 | const int _PG_BOOL = 16; 81 | const int _PG_BYTEA = 17; 82 | const int _PG_CHAR = 18; 83 | const int _PG_INT8 = 20; 84 | const int _PG_INT2 = 21; 85 | const int _PG_INT4 = 23; 86 | const int _PG_TEXT = 25; 87 | const int _PG_FLOAT4 = 700; 88 | const int _PG_FLOAT8 = 701; 89 | const int _PG_INTERVAL = 704; 90 | const int _PG_UNKNOWN = 705; 91 | const int _PG_MONEY = 790; 92 | const int _PG_VARCHAR = 1043; 93 | const int _PG_DATE = 1082; 94 | const int _PG_TIME = 1083; 95 | const int _PG_TIMESTAMP = 1114; 96 | const int _PG_TIMESTAMPZ = 1184; 97 | const int _PG_TIMETZ = 1266; 98 | const int _PG_NUMERIC = 1700; 99 | const int _PG_JSON = 114; 100 | const int _PG_JSONB = 3802; 101 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/messages.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.impl; 2 | 3 | 4 | class ClientMessageImpl implements ClientMessage { 5 | 6 | ClientMessageImpl( 7 | {this.isError: false, 8 | this.severity, 9 | this.message, 10 | this.connectionName, 11 | this.exception, 12 | this.stackTrace}) { 13 | 14 | if (isError == null) throw new ArgumentError.notNull('isError'); 15 | 16 | if (severity != 'ERROR' && severity != 'WARNING' && severity != 'DEBUG') 17 | throw new ArgumentError.notNull('severity'); 18 | 19 | if (message == null) throw new ArgumentError.notNull('message'); 20 | } 21 | 22 | final bool isError; 23 | final String severity; 24 | final String message; 25 | final String connectionName; 26 | final exception; 27 | final StackTrace stackTrace; 28 | 29 | String toString() => connectionName == null 30 | ? '$severity $message' 31 | : '$connectionName $severity $message'; 32 | } 33 | 34 | class ServerMessageImpl implements ServerMessage { 35 | 36 | ServerMessageImpl(this.isError, Map fields, [this.connectionName]) 37 | : fields = new UnmodifiableMapView(fields), 38 | severity = fields['S'], 39 | code = fields['C'], 40 | message = fields['M']; 41 | 42 | final bool isError; 43 | final String connectionName; 44 | final Map fields; 45 | 46 | final String severity; 47 | final String code; 48 | final String message; 49 | 50 | String get detail => fields['D']; 51 | String get hint => fields['H']; 52 | String get position => fields['P']; 53 | String get internalPosition => fields['p']; 54 | String get internalQuery => fields['q']; 55 | String get where => fields['W']; 56 | String get schema => fields['s']; 57 | String get table => fields['t']; 58 | String get column => fields['c']; 59 | String get dataType => fields['d']; 60 | String get constraint => fields['n']; 61 | String get file => fields['F']; 62 | String get line => fields['L']; 63 | String get routine => fields['R']; 64 | 65 | String toString() => connectionName == null 66 | ? '$severity $code $message' 67 | : '$connectionName $severity $code $message'; 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/postgresql_impl.dart: -------------------------------------------------------------------------------- 1 | library postgresql.impl; 2 | 3 | import 'dart:async'; 4 | import 'dart:collection'; 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | import 'package:convert/convert.dart'; 8 | import 'package:crypto/crypto.dart'; 9 | import 'package:postgresql/postgresql.dart'; 10 | import 'package:postgresql/constants.dart'; 11 | import 'package:postgresql/src/substitute.dart'; 12 | import 'package:postgresql/src/buffer.dart'; 13 | 14 | part 'connection.dart'; 15 | part 'constants.dart'; 16 | part 'messages.dart'; 17 | part 'query.dart'; 18 | part 'settings.dart'; 19 | part 'type_converter.dart'; 20 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/query.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.impl; 2 | 3 | class _Query { 4 | int _state = _QUEUED; 5 | final String sql; 6 | final StreamController<_Row> _controller = new StreamController<_Row>(); 7 | int _commandIndex = 0; 8 | int _columnCount; 9 | List<_Column> _columns; 10 | List _rowData; 11 | int _rowsAffected; 12 | 13 | List _columnNames; 14 | Map _columnIndex; 15 | 16 | _Query(this.sql); 17 | 18 | Stream get stream => _controller.stream; 19 | 20 | void addRowDescription() { 21 | if (_state == _QUEUED) 22 | _state = _STREAMING; 23 | 24 | _columnNames = _columns.map((c) => c.name).toList(); 25 | 26 | var ident = new RegExp(r'^[a-zA-Z][a-zA-Z0-9_]*$'); 27 | _columnIndex = new Map(); 28 | for (var i = 0; i < _columnNames.length; i++) { 29 | var name = _columnNames[i]; 30 | if (ident.hasMatch(name)) 31 | _columnIndex[new Symbol(name)] = i; 32 | } 33 | } 34 | 35 | void addRow() { 36 | var row = new _Row(_columnNames, _rowData, _columnIndex, _columns); 37 | _rowData = null; 38 | _controller.add(row); 39 | } 40 | 41 | void addError(Object err) { 42 | _controller.addError(err); 43 | // stream will be closed once the ready for query message is received. 44 | } 45 | 46 | void close() { 47 | _controller.close(); 48 | _state = _DONE; 49 | } 50 | } 51 | 52 | //TODO rename to field, as it may not be a column. 53 | class _Column implements Column { 54 | final int index; 55 | final String name; 56 | 57 | //TODO figure out what to name these. 58 | // Perhaps just use libpq names as they will be documented in existing code 59 | // examples. It may not be neccesary to store all of this info. 60 | final int fieldId; 61 | final int tableColNo; 62 | final int fieldType; 63 | final int dataSize; 64 | final int typeModifier; 65 | final int formatCode; 66 | 67 | bool get isBinary => formatCode == 1; 68 | 69 | _Column(this.index, this.name, this.fieldId, this.tableColNo, this.fieldType, this.dataSize, this.typeModifier, this.formatCode); 70 | 71 | String toString() => 'Column: index: $index, name: $name, fieldId: $fieldId, tableColNo: $tableColNo, fieldType: $fieldType, dataSize: $dataSize, typeModifier: $typeModifier, formatCode: $formatCode.'; 72 | } 73 | 74 | class _Row implements Row { 75 | _Row(this._columnNames, this._columnValues, this._index, this._columns) { 76 | assert(this._columnNames.length == this._columnValues.length); 77 | } 78 | 79 | // Map column name to column index 80 | final Map _index; 81 | final List _columnNames; 82 | final List _columnValues; 83 | final List _columns; 84 | 85 | operator[] (int i) => _columnValues[i]; 86 | 87 | void forEach(void f(String columnName, columnValue)) { 88 | assert(_columnValues.length == _columnNames.length); 89 | for (int i = 0; i < _columnValues.length; i++) { 90 | f(_columnNames[i], _columnValues[i]); 91 | } 92 | } 93 | 94 | noSuchMethod(Invocation invocation) { 95 | var name = invocation.memberName; 96 | if (invocation.isGetter) { 97 | var i = _index[name]; 98 | if (i != null) 99 | return _columnValues[i]; 100 | } 101 | super.noSuchMethod(invocation); 102 | } 103 | 104 | String toString() => _columnValues.toString(); 105 | 106 | List toList() => new UnmodifiableListView(_columnValues); 107 | 108 | Map toMap() => new Map.fromIterables(_columnNames, _columnValues); 109 | 110 | List getColumns() => new UnmodifiableListView(_columns) as List; 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/settings.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.impl; 2 | 3 | class SettingsImpl implements Settings { 4 | String _host; 5 | int _port; 6 | String _user; 7 | String _password; 8 | String _database; 9 | bool _requireSsl; 10 | 11 | static const String DEFAULT_HOST = 'localhost'; 12 | static const String HOST = 'host'; 13 | static const String PORT = 'port'; 14 | static const String USER = 'user'; 15 | static const String PASSWORD = 'password'; 16 | static const String DATABASE = 'database'; 17 | 18 | SettingsImpl(this._host, 19 | this._port, 20 | this._user, 21 | this._password, 22 | this._database, 23 | {bool requireSsl: false}) 24 | : _requireSsl = requireSsl; 25 | 26 | static _error(msg) => new PostgresqlException('Settings: $msg', null); 27 | 28 | factory SettingsImpl.fromUri(String uri) { 29 | 30 | var u = Uri.parse(uri); 31 | if (u.scheme != 'postgres' && u.scheme != 'postgresql') 32 | throw _error('Invalid uri: scheme must be `postgres` or `postgresql`.'); 33 | 34 | if (u.userInfo == null || u.userInfo == '') 35 | throw _error('Invalid uri: username must be specified.'); 36 | 37 | var userInfo; 38 | if (u.userInfo.contains(':')) 39 | userInfo = u.userInfo.split(':'); 40 | else userInfo = [u.userInfo, '']; 41 | 42 | if (u.path == null || !u.path.startsWith('/') || !(u.path.length > 1)) 43 | throw _error('Invalid uri: `database name must be specified`.'); 44 | 45 | bool requireSsl = false; 46 | if (u.query != null) 47 | requireSsl = u.query.contains('sslmode=require'); 48 | 49 | return new Settings( 50 | Uri.decodeComponent(u.host), 51 | u.port == 0 ? Settings.defaultPort : u.port, 52 | Uri.decodeComponent(userInfo[0]), 53 | Uri.decodeComponent(userInfo[1]), 54 | Uri.decodeComponent(u.path.substring(1)), // Remove preceding forward slash. 55 | requireSsl: requireSsl); 56 | } 57 | 58 | SettingsImpl.fromMap(Map config){ 59 | 60 | final String host = config.containsKey(HOST) ? 61 | config[HOST] : DEFAULT_HOST; 62 | final int port = config.containsKey(PORT) ? 63 | config[PORT] is int ? config[PORT] 64 | : throw _error('Specified port is not a valid number') 65 | : Settings.defaultPort; 66 | if (!config.containsKey(USER)) 67 | throw _error(USER); 68 | if (!config.containsKey(PASSWORD)) 69 | throw _error(PASSWORD); 70 | if (!config.containsKey(DATABASE)) 71 | throw _error(DATABASE); 72 | 73 | this._host = host; 74 | this._port = port; 75 | this._user = config[USER]; 76 | 77 | var pwd = config[PASSWORD]; 78 | this._password = pwd == null || pwd == '' ? '' : pwd; 79 | this._database = config[DATABASE]; 80 | 81 | this._requireSsl = config.containsKey('sslmode') 82 | && config['sslmode'] == 'require'; 83 | } 84 | 85 | String get host => _host; 86 | int get port => _port; 87 | String get user => _user; 88 | String get password => _password; 89 | String get database => _database; 90 | bool get requireSsl => _requireSsl; 91 | 92 | String toUri() => new Uri( 93 | scheme: 'postgres', 94 | userInfo: _password == null || _password == '' 95 | ? '$_user' 96 | : '$_user:$_password', 97 | host: _host, 98 | port: _port, 99 | path: _database, 100 | query: requireSsl ? '?sslmode=require' : null).toString(); 101 | 102 | String toString() 103 | => "Settings {host: $_host, port: $_port, user: $_user, database: $_database}"; 104 | 105 | Map toMap() { 106 | var map = new Map(); 107 | map[HOST] = host; 108 | map[PORT] = port; 109 | map[USER] = user; 110 | map[PASSWORD] = password; 111 | map[DATABASE] = database; 112 | if (requireSsl) 113 | map['sslmode'] = 'require'; 114 | return map; 115 | } 116 | 117 | Map toJson() => toMap(); 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/postgresql_impl/type_converter.dart: -------------------------------------------------------------------------------- 1 | part of postgresql.impl; 2 | 3 | const int _apos = 39; 4 | const int _return = 13; 5 | const int _newline = 10; 6 | const int _backslash = 92; 7 | 8 | final _escapeRegExp = new RegExp(r"['\r\n\\]"); 9 | 10 | class RawTypeConverter extends DefaultTypeConverter { 11 | String encode(value, String type, {getConnectionName()}) 12 | => encodeValue(value, type); 13 | 14 | Object decode(String value, int pgType, {bool isUtcTimeZone: false, 15 | getConnectionName()}) => value; 16 | } 17 | 18 | String encodeString(String s) { 19 | if (s == null) return ' null '; 20 | 21 | var escaped = s.replaceAllMapped(_escapeRegExp, (m) { 22 | switch (s.codeUnitAt(m.start)) { 23 | case _apos: return r"\'"; 24 | case _return: return r'\r'; 25 | case _newline: return r'\n'; 26 | case _backslash: return r'\\'; 27 | default: assert(false); 28 | } 29 | }); 30 | 31 | return " E'$escaped' "; 32 | } 33 | 34 | 35 | class DefaultTypeConverter implements TypeConverter { 36 | 37 | String encode(value, String type, {getConnectionName()}) 38 | => encodeValue(value, type, getConnectionName: getConnectionName); 39 | 40 | Object decode(String value, int pgType, {bool isUtcTimeZone: false, 41 | getConnectionName()}) => decodeValue(value, pgType, 42 | isUtcTimeZone: isUtcTimeZone, getConnectionName: getConnectionName); 43 | 44 | PostgresqlException _error(String msg, getConnectionName()) { 45 | var name = getConnectionName == null ? null : getConnectionName(); 46 | return new PostgresqlException(msg, name); 47 | } 48 | 49 | String encodeValue(value, String type, {getConnectionName()}) { 50 | 51 | if (type == null) 52 | return encodeValueDefault(value, getConnectionName: getConnectionName); 53 | 54 | throwError() => throw _error('Invalid runtime type and type modifier ' 55 | 'combination (${value.runtimeType} to $type).', getConnectionName); 56 | 57 | if (value == null) 58 | return 'null'; 59 | 60 | if (type != null) 61 | type = type.toLowerCase(); 62 | 63 | if (type == 'text' || type == 'string') 64 | return encodeString(value.toString()); 65 | 66 | if (type == 'integer' 67 | || type == 'smallint' 68 | || type == 'bigint' 69 | || type == 'serial' 70 | || type == 'bigserial' 71 | || type == 'int') { 72 | if (value is! int) throwError(); 73 | return encodeNumber(value); 74 | } 75 | 76 | if (type == 'real' 77 | || type == 'double' 78 | || type == 'num' 79 | || type == 'number') { 80 | if (value is! num) throwError(); 81 | return encodeNumber(value); 82 | } 83 | 84 | // TODO numeric, decimal 85 | 86 | if (type == 'boolean' || type == 'bool') { 87 | if (value is! bool) throwError(); 88 | return value.toString(); 89 | } 90 | 91 | if (type == 'timestamp' || type == 'timestamptz' || type == 'datetime') { 92 | if (value is! DateTime) throwError(); 93 | return encodeDateTime(value, isDateOnly: false); 94 | } 95 | 96 | if (type == 'date') { 97 | if (value is! DateTime) throwError(); 98 | return encodeDateTime(value, isDateOnly: true); 99 | } 100 | 101 | if (type == 'json' || type == 'jsonb') 102 | return encodeValueToJson(value); 103 | 104 | // if (type == 'bytea') { 105 | // if (value is! List) throwError(); 106 | // return encodeBytea(value); 107 | // } 108 | // 109 | // if (type == 'array') { 110 | // if (value is! List) throwError(); 111 | // return encodeArray(value); 112 | // } 113 | 114 | throw _error('Unknown type name: $type.', getConnectionName); 115 | } 116 | 117 | // Unspecified type name. Use default type mapping. 118 | String encodeValueDefault(value, {getConnectionName()}) { 119 | 120 | if (value == null) 121 | return 'null'; 122 | 123 | if (value is num) 124 | return encodeNumber(value); 125 | 126 | if (value is String) 127 | return encodeString(value); 128 | 129 | if (value is DateTime) 130 | return encodeDateTime(value, isDateOnly: false); 131 | 132 | if (value is bool) 133 | return value.toString(); 134 | 135 | if (value is Map) 136 | return encodeString(JSON.encode(value)); 137 | 138 | if (value is List) 139 | return encodeArray(value); 140 | 141 | throw _error('Unsupported runtime type as query parameter ' 142 | '(${value.runtimeType}).', getConnectionName); 143 | } 144 | 145 | //FIXME can probably simplify this, as in postgresql json type must take 146 | // map or array at top level, not string or number. (I think???) 147 | String encodeValueToJson(value, {getConnectionName()}) { 148 | if (value == null) 149 | return "'null'"; 150 | 151 | if (value is Map || value is List) 152 | return encodeString(JSON.encode(value)); 153 | 154 | if (value is String) 155 | return encodeString('"$value"'); 156 | 157 | if (value is num) { 158 | // These are not valid JSON numbers, so encode them as strings. 159 | // TODO consider throwing an error instead. 160 | if (value.isNaN) return '"nan"'; 161 | if (value == double.INFINITY) return '"infinity"'; 162 | if (value == double.NEGATIVE_INFINITY) return '"-infinity"'; 163 | return value.toString(); 164 | } 165 | 166 | try { 167 | return encodeString(JSON.encode(value)); 168 | } catch (e) { 169 | throw _error('Could not convert object to JSON. ' 170 | 'No toJson() method was implemented on the object.', getConnectionName); 171 | } 172 | } 173 | 174 | String encodeNumber(num n) { 175 | if (n.isNaN) return "'nan'"; 176 | if (n == double.INFINITY) return "'infinity'"; 177 | if (n == double.NEGATIVE_INFINITY) return "'-infinity'"; 178 | return "${n.toString()}"; 179 | } 180 | 181 | String encodeArray(List value) { 182 | //TODO implement postgresql array types 183 | throw _error('Postgresql array types not implemented yet. ' 184 | 'Pull requests welcome ;)', null); 185 | } 186 | 187 | String encodeDateTime(DateTime datetime, {bool isDateOnly}) { 188 | 189 | if (datetime == null) 190 | return 'null'; 191 | 192 | var string = datetime.toIso8601String(); 193 | 194 | if (isDateOnly) { 195 | string = string.split("T").first; 196 | } else { 197 | 198 | // ISO8601 UTC times already carry Z, but local times carry no timezone info 199 | // so this code will append it. 200 | if (!datetime.isUtc) { 201 | var timezoneHourOffset = datetime.timeZoneOffset.inHours; 202 | var timezoneMinuteOffset = datetime.timeZoneOffset.inMinutes % 60; 203 | 204 | // Note that the sign is stripped via abs() and appended later. 205 | var hourComponent = timezoneHourOffset.abs().toString().padLeft(2, "0"); 206 | var minuteComponent = timezoneMinuteOffset.abs().toString().padLeft(2, "0"); 207 | 208 | if (timezoneHourOffset >= 0) { 209 | hourComponent = "+${hourComponent}"; 210 | } else { 211 | hourComponent = "-${hourComponent}"; 212 | } 213 | 214 | var timezoneString = [hourComponent, minuteComponent].join(":"); 215 | string = [string, timezoneString].join(""); 216 | } 217 | } 218 | 219 | if (string.substring(0, 1) == "-") { 220 | // Postgresql uses a BC suffix for dates rather than the negative prefix returned by 221 | // dart's ISO8601 date string. 222 | string = string.substring(1) + " BC"; 223 | } else if (string.substring(0, 1) == "+") { 224 | // Postgresql doesn't allow leading + signs for 6 digit dates. Strip it out. 225 | string = string.substring(1); 226 | } 227 | 228 | return "'${string}'"; 229 | } 230 | 231 | // See http://www.postgresql.org/docs/9.0/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE 232 | String encodeBytea(List value) { 233 | 234 | //var b64String = ...; 235 | //return " decode('$b64String', 'base64') "; 236 | 237 | throw _error('bytea encoding not implemented. Pull requests welcome ;)', null); 238 | } 239 | 240 | Object decodeValue(String value, int pgType, 241 | {bool isUtcTimeZone, getConnectionName()}) { 242 | 243 | switch (pgType) { 244 | 245 | case _PG_BOOL: 246 | return value == 't'; 247 | 248 | case _PG_INT2: // smallint 249 | case _PG_INT4: // integer 250 | case _PG_INT8: // bigint 251 | return int.parse(value); 252 | 253 | case _PG_FLOAT4: // real 254 | case _PG_FLOAT8: // double precision 255 | return double.parse(value); 256 | 257 | case _PG_TIMESTAMP: 258 | case _PG_TIMESTAMPZ: 259 | case _PG_DATE: 260 | return decodeDateTime(value, pgType, 261 | isUtcTimeZone: isUtcTimeZone, getConnectionName: getConnectionName); 262 | 263 | case _PG_JSON: 264 | case _PG_JSONB: 265 | return JSON.decode(value); 266 | 267 | // Not implemented yet - return a string. 268 | case _PG_MONEY: 269 | case _PG_TIMETZ: 270 | case _PG_TIME: 271 | case _PG_INTERVAL: 272 | case _PG_NUMERIC: 273 | 274 | //TODO arrays 275 | //TODO binary bytea 276 | 277 | default: 278 | // Return a string for unknown types. The end user can parse this. 279 | return value; 280 | } 281 | } 282 | 283 | DateTime decodeDateTime(String value, int pgType, {bool isUtcTimeZone, getConnectionName()}) { 284 | // Built in Dart dates can either be local time or utc. Which means that the 285 | // the postgresql timezone parameter for the connection must be either set 286 | // to UTC, or the local time of the server on which the client is running. 287 | // This restriction could be relaxed by using a more advanced date library 288 | // capable of creating DateTimes for a non-local time zone. 289 | 290 | if (value == 'infinity' || value == '-infinity') { 291 | throw _error('Server returned a timestamp with value ' 292 | '"$value", this cannot be represented as a dart date object, if ' 293 | 'infinity values are required, rewrite the sql query to cast the ' 294 | 'value to a string, i.e. col::text.', getConnectionName); 295 | } 296 | 297 | var formattedValue = value; 298 | 299 | // Postgresql uses a BC suffix rather than a negative prefix as in ISO8601. 300 | if (value.endsWith(' BC')) formattedValue = '-' + value.substring(0, value.length - 3); 301 | 302 | if (pgType == _PG_TIMESTAMP) { 303 | formattedValue += 'Z'; 304 | } else if (pgType == _PG_TIMESTAMPZ) { 305 | // PG will return the timestamp in the connection's timezone. The resulting DateTime.parse will handle accordingly. 306 | } else if (pgType == _PG_DATE) { 307 | formattedValue = formattedValue + 'T00:00:00Z'; 308 | } 309 | 310 | return DateTime.parse(formattedValue); 311 | } 312 | 313 | } 314 | -------------------------------------------------------------------------------- /lib/src/protocol.dart: -------------------------------------------------------------------------------- 1 | library postgresql.protocol; 2 | 3 | import 'dart:collection'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | 7 | // http://www.postgresql.org/docs/9.2/static/protocol-message-formats.html 8 | 9 | //TODO Swap the connection class over to using these. 10 | // currently just used for testing. 11 | 12 | //Map frontendMessages = { 13 | //}; 14 | // 15 | //Map backedMessages = { 16 | //}; 17 | 18 | 19 | abstract class ProtocolMessage { 20 | int get messageCode; 21 | List encode(); 22 | 23 | //TODO 24 | static ProtocolMessage decodeFrontend(List buffer, int offset) 25 | => throw new UnimplementedError(); 26 | 27 | //TODO 28 | static ProtocolMessage decodeBackend(List buffer, int offset) 29 | => throw new UnimplementedError(); 30 | } 31 | 32 | class Startup implements ProtocolMessage { 33 | 34 | Startup(this.user, this.database, [this.parameters = const {}]) { 35 | if (user == null || database == null) throw new ArgumentError(); 36 | } 37 | 38 | // Startup and ssl request are the only messages without a messageCode. 39 | final int messageCode = 0; 40 | final int protocolVersion = 196608; 41 | final String user; 42 | final String database; 43 | final Map parameters; 44 | 45 | List encode() { 46 | var mb = new _MessageBuilder(messageCode) 47 | ..addInt32(protocolVersion) 48 | ..addUtf8('user') 49 | ..addUtf8(user) 50 | ..addUtf8('database') 51 | ..addUtf8(database) 52 | ..addUtf8('client_encoding') 53 | ..addUtf8('UTF8'); 54 | parameters.forEach((k, v) { 55 | mb.addUtf8(k); 56 | mb.addUtf8(v); 57 | }); 58 | mb.addByte(0); 59 | 60 | return mb.build(); 61 | } 62 | 63 | String toString() => JSON.encode({ 64 | 'msg': runtimeType.toString(), 65 | 'code': new String.fromCharCode(messageCode), 66 | 'protocolVersion': protocolVersion, 67 | 'user': user, 68 | 'database': database 69 | }); 70 | } 71 | 72 | class SslRequest implements ProtocolMessage { 73 | // Startup and ssl request are the only messages without a messageCode. 74 | final int messageCode = 0; 75 | List encode() => [0, 0, 0, 8, 4, 210, 22, 47]; 76 | String toString() => JSON.encode({ 77 | 'msg': runtimeType.toString(), 78 | 'code': new String.fromCharCode(messageCode), 79 | }); 80 | } 81 | 82 | class Terminate implements ProtocolMessage { 83 | final int messageCode = 'X'.codeUnitAt(0); 84 | List encode() => new _MessageBuilder(messageCode).build(); 85 | String toString() => JSON.encode({ 86 | 'msg': runtimeType.toString(), 87 | 'code': new String.fromCharCode(messageCode), 88 | }); 89 | } 90 | 91 | const int authTypeOk = 0; 92 | const int authTypeMd5 = 5; 93 | 94 | //const int authOk = 0; 95 | //const int authKerebosV5 = 2; 96 | //const int authScm = 6; 97 | //const int authGss = 7; 98 | //const int authClearText = 3; 99 | 100 | 101 | class AuthenticationRequest implements ProtocolMessage { 102 | 103 | AuthenticationRequest.ok() : authType = authTypeOk, salt = null; 104 | 105 | AuthenticationRequest.md5(this.salt) 106 | : authType = authTypeMd5 { 107 | if (salt == null || salt.length != 4) throw new ArgumentError(); 108 | } 109 | 110 | final int messageCode = 'R'.codeUnitAt(0); 111 | final int authType; 112 | final List salt; 113 | 114 | List encode() { 115 | var mb = new _MessageBuilder(messageCode); 116 | mb.addInt32(authType); 117 | if (authType == authTypeMd5) mb.addBytes(salt); 118 | return mb.build(); 119 | } 120 | 121 | String toString() => JSON.encode({ 122 | 'msg': runtimeType.toString(), 123 | 'code': new String.fromCharCode(messageCode), 124 | 'authType': {0: "ok", 5: "md5"}[authType], 125 | 'salt': salt 126 | }); 127 | } 128 | 129 | class BackendKeyData implements ProtocolMessage { 130 | 131 | BackendKeyData(this.backendPid, this.secretKey) { 132 | if (backendPid == null || secretKey == null) throw new ArgumentError(); 133 | } 134 | 135 | final int messageCode = 'K'.codeUnitAt(0); 136 | final int backendPid; 137 | final int secretKey; 138 | 139 | List encode() { 140 | var mb = new _MessageBuilder(messageCode) 141 | ..addInt32(backendPid) 142 | ..addInt32(secretKey); 143 | return mb.build(); 144 | } 145 | 146 | String toString() => JSON.encode({ 147 | 'msg': runtimeType.toString(), 148 | 'code': new String.fromCharCode(messageCode), 149 | 'backendPid': backendPid, 150 | 'secretKey': secretKey 151 | }); 152 | } 153 | 154 | class ParameterStatus implements ProtocolMessage { 155 | 156 | ParameterStatus(this.name, this.value); 157 | 158 | final int messageCode = 'S'.codeUnitAt(0); 159 | final String name; 160 | final String value; 161 | 162 | List encode() { 163 | var mb = new _MessageBuilder(messageCode) 164 | ..addUtf8(name) 165 | ..addUtf8(value); 166 | return mb.build(); 167 | } 168 | 169 | String toString() => JSON.encode({ 170 | 'msg': runtimeType.toString(), 171 | 'code': new String.fromCharCode(messageCode), 172 | 'name': name, 173 | 'value': value}); 174 | } 175 | 176 | 177 | class Query implements ProtocolMessage { 178 | 179 | Query(this.query); 180 | 181 | final int messageCode = 'Q'.codeUnitAt(0); 182 | final String query; 183 | 184 | List encode() 185 | => (new _MessageBuilder(messageCode)..addUtf8(query)).build(); //FIXME why do I need extra parens here. Analyzer bug? 186 | 187 | String toString() => JSON.encode({ 188 | 'msg': runtimeType.toString(), 189 | 'code': new String.fromCharCode(messageCode), 190 | 'query': query}); 191 | } 192 | 193 | 194 | class Field { 195 | Field(this.name, this.fieldType); 196 | final String name; 197 | final int fieldId = 0; 198 | final int tableColNo = 0; 199 | final int fieldType; 200 | final int dataSize = -1; 201 | final int typeModifier = 0; 202 | final int formatCode = 0; 203 | bool get isBinary => formatCode == 1; 204 | 205 | String toString() => JSON.encode({ 206 | 'name': name, 207 | 'fieldId': fieldId, 208 | 'tableColNo': tableColNo, 209 | 'fieldType': fieldType, 210 | 'dataSize': dataSize, 211 | 'typeModifier': typeModifier, 212 | 'formatCode': formatCode 213 | }); 214 | } 215 | 216 | class RowDescription implements ProtocolMessage { 217 | 218 | RowDescription(this.fields) { 219 | if (fields == null) throw new ArgumentError(); 220 | } 221 | 222 | final int messageCode = 'T'.codeUnitAt(0); 223 | final List fields; 224 | 225 | List encode() { 226 | var mb = new _MessageBuilder(messageCode) 227 | ..addInt16(fields.length); 228 | 229 | for (var f in fields) { 230 | mb..addUtf8(f.name) 231 | ..addInt32(f.fieldId) 232 | ..addInt16(f.tableColNo) 233 | ..addInt32(f.fieldType) 234 | ..addInt16(f.dataSize) 235 | ..addInt32(f.typeModifier) 236 | ..addInt16(f.formatCode); 237 | } 238 | 239 | return mb.build(); 240 | } 241 | 242 | String toString() => JSON.encode({ 243 | 'msg': runtimeType.toString(), 244 | 'code': new String.fromCharCode(messageCode), 245 | 'fields': fields}); 246 | } 247 | 248 | 249 | // Performance DataRows. multiple rows aggregated into one. 250 | 251 | class DataRow implements ProtocolMessage { 252 | 253 | DataRow.fromBytes(this.values) { 254 | if (values == null) throw new ArgumentError(); 255 | } 256 | 257 | DataRow.fromStrings(List strings) 258 | : values = strings.map(UTF8.encode).toList(growable: false); 259 | 260 | final int messageCode = 'D'.codeUnitAt(0); 261 | final List> values; 262 | 263 | List encode() { 264 | var mb = new _MessageBuilder(messageCode) 265 | ..addInt16(values.length); 266 | 267 | for (var bytes in values) { 268 | mb..addInt32(bytes.length) 269 | ..addBytes(bytes); 270 | } 271 | 272 | return mb.build(); 273 | } 274 | 275 | String toString() => JSON.encode({ 276 | 'msg': runtimeType.toString(), 277 | 'code': new String.fromCharCode(messageCode), 278 | 'values': values.map(UTF8.decode) //TODO not all DataRows are text, some are binary. 279 | }); 280 | } 281 | 282 | 283 | class CommandComplete implements ProtocolMessage { 284 | 285 | CommandComplete(this.tag); 286 | 287 | CommandComplete.insert(int oid, int rows) : this('INSERT $oid $rows'); 288 | CommandComplete.delete(int rows) : this('DELETE $rows'); 289 | CommandComplete.update(int rows) : this('UPDATE $rows'); 290 | CommandComplete.select(int rows) : this('SELECT $rows'); 291 | CommandComplete.move(int rows) : this('MOVE $rows'); 292 | CommandComplete.fetch(int rows) : this('FETCH $rows'); 293 | CommandComplete.copy(int rows) : this('COPY $rows'); 294 | 295 | final int messageCode = 'C'.codeUnitAt(0); 296 | final String tag; 297 | 298 | List encode() => (new _MessageBuilder(messageCode)..addUtf8(tag)).build(); //FIXME remove extra parens. 299 | 300 | String toString() => JSON.encode({ 301 | 'msg': runtimeType.toString(), 302 | 'code': new String.fromCharCode(messageCode), 303 | 'tag': tag 304 | }); 305 | } 306 | 307 | enum TransactionStatus { none, transaction, failed } 308 | 309 | class ReadyForQuery implements ProtocolMessage { 310 | 311 | ReadyForQuery(this.transactionStatus); 312 | 313 | final int messageCode = 'Z'.codeUnitAt(0); 314 | final TransactionStatus transactionStatus; 315 | 316 | static final Map _txStatus = { 317 | TransactionStatus.none: 'I'.codeUnitAt(0), 318 | TransactionStatus.transaction: 'T'.codeUnitAt(0), 319 | TransactionStatus.failed: 'E'.codeUnitAt(0) 320 | }; 321 | 322 | List encode() { 323 | var mb = new _MessageBuilder(messageCode) 324 | ..addByte(_txStatus[transactionStatus]); 325 | return mb.build(); 326 | } 327 | 328 | String toString() => JSON.encode({ 329 | 'msg': runtimeType.toString(), 330 | 'code': new String.fromCharCode(messageCode), 331 | 'transactionStatus': _txStatus[transactionStatus] 332 | }); 333 | } 334 | 335 | abstract class BaseResponse implements ProtocolMessage { 336 | 337 | BaseResponse(Map fields) 338 | : fields = new UnmodifiableMapView(fields) { 339 | if (fields == null) throw new ArgumentError(); 340 | assert(fields.keys.every((k) => k.length == 1)); 341 | } 342 | 343 | String get severity => fields['S']; 344 | String get code => fields['C']; 345 | String get message => fields['M']; 346 | String get detail => fields['D']; 347 | String get hint => fields['H']; 348 | String get position => fields['P']; 349 | String get internalPosition => fields['p']; 350 | String get internalQuery => fields['q']; 351 | String get where => fields['W']; 352 | String get schema => fields['s']; 353 | String get table => fields['t']; 354 | String get column => fields['c']; 355 | String get dataType => fields['d']; 356 | String get constraint => fields['n']; 357 | String get file => fields['F']; 358 | String get line => fields['L']; 359 | String get routine => fields['R']; 360 | 361 | final Map fields; 362 | 363 | List encode() { 364 | var mb = new _MessageBuilder(messageCode); 365 | fields.forEach((k, v) => mb..addUtf8(k)..addUtf8(v)); 366 | mb.addByte(0); // Terminator 367 | return mb.build(); 368 | } 369 | 370 | String toString() => JSON.encode({ 371 | 'msg': runtimeType.toString(), 372 | 'code': new String.fromCharCode(messageCode), 373 | 'fields': fields 374 | }); 375 | } 376 | 377 | class ErrorResponse extends BaseResponse implements ProtocolMessage { 378 | ErrorResponse(Map fields) : super(fields); 379 | final int messageCode = 'E'.codeUnitAt(0); 380 | } 381 | 382 | class NoticeResponse extends BaseResponse implements ProtocolMessage { 383 | NoticeResponse(Map fields) : super(fields); 384 | final int messageCode = 'N'.codeUnitAt(0); 385 | } 386 | 387 | class EmptyQueryResponse implements ProtocolMessage { 388 | final int messageCode = 'I'.codeUnitAt(0); 389 | List encode() => new _MessageBuilder(messageCode).build(); 390 | 391 | String toString() => JSON.encode({ 392 | 'msg': runtimeType.toString(), 393 | 'code': new String.fromCharCode(messageCode) 394 | }); 395 | } 396 | 397 | 398 | class _MessageBuilder { 399 | 400 | _MessageBuilder(this._messageCode) { 401 | // All messages other than startup have a message code header. 402 | if (_messageCode != 0) 403 | _builder.addByte(_messageCode); 404 | 405 | // Add a padding for filling in the length during build. 406 | _builder.add(const [0, 0, 0, 0]); 407 | } 408 | 409 | final int _messageCode; 410 | 411 | //TODO experiment with disabling copy for performance. 412 | //Probably better just to do for large performance sensitive message types. 413 | final BytesBuilder _builder = new BytesBuilder(copy: true); 414 | 415 | void addByte(int byte) { 416 | assert(byte >= 0 && byte < 256); 417 | _builder.addByte(byte); 418 | } 419 | 420 | void addInt16(int i) { 421 | assert(i >= -32768 && i <= 32767); 422 | 423 | if (i < 0) i = 0x10000 + i; 424 | 425 | int a = (i >> 8) & 0x00FF; 426 | int b = i & 0x00FF; 427 | 428 | _builder.addByte(a); 429 | _builder.addByte(b); 430 | } 431 | 432 | void addInt32(int i) { 433 | assert(i >= -2147483648 && i <= 2147483647); 434 | 435 | if (i < 0) i = 0x100000000 + i; 436 | 437 | int a = (i >> 24) & 0x000000FF; 438 | int b = (i >> 16) & 0x000000FF; 439 | int c = (i >> 8) & 0x000000FF; 440 | int d = i & 0x000000FF; 441 | 442 | _builder.addByte(a); 443 | _builder.addByte(b); 444 | _builder.addByte(c); 445 | _builder.addByte(d); 446 | } 447 | 448 | /// Add a null terminated string. 449 | void addUtf8(String s) { 450 | // Postgresql server must be configured to accept UTF8 - this is the default. 451 | _builder.add(UTF8.encode(s)); 452 | addByte(0); 453 | } 454 | 455 | void addBytes(List bytes) { 456 | _builder.add(bytes); 457 | } 458 | 459 | List build() { 460 | var bytes = _builder.toBytes(); 461 | 462 | int offset = 0; 463 | int i = bytes.length; 464 | 465 | if (_messageCode != 0) { 466 | offset = 1; 467 | i -= 1; 468 | } 469 | 470 | bytes[offset] = (i >> 24) & 0x000000FF; 471 | bytes[offset + 1] = (i >> 16) & 0x000000FF; 472 | bytes[offset + 2] = (i >> 8) & 0x000000FF; 473 | bytes[offset + 3] = i & 0x000000FF; 474 | 475 | return bytes; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /lib/src/substitute.dart: -------------------------------------------------------------------------------- 1 | library postgresql.substitute; 2 | 3 | const int _TOKEN_TEXT = 1; 4 | const int _TOKEN_AT = 2; 5 | const int _TOKEN_IDENT = 3; 6 | 7 | const int _a = 97; 8 | const int _A = 65; 9 | const int _z = 122; 10 | const int _Z = 90; 11 | const int _0 = 48; 12 | const int _9 = 57; 13 | const int _at = 64; 14 | const int _colon = 58; 15 | const int _underscore = 95; 16 | 17 | class _Token { 18 | _Token(this.type, this.value, [this.typeName]); 19 | final int type; 20 | final String value; 21 | final String typeName; 22 | String toString() => '${['?', 'Text', 'At', 'Ident'][type]} "$value" "$typeName"'; 23 | } 24 | 25 | bool isIdentifier(int charCode) => (charCode >= _a && charCode <= _z) 26 | || (charCode >= _A && charCode <= _Z) 27 | || (charCode >= _0 && charCode <= _9) 28 | || (charCode == _underscore); 29 | 30 | bool isDigit(int charCode) => (charCode >= _0 && charCode <= _9); 31 | 32 | class ParseException { 33 | ParseException(this.message, [this.source, this.index]); 34 | final String message; 35 | final String source; 36 | final int index; 37 | String toString() => (source == null || index == null) 38 | ? message 39 | : '$message At character: $index, in source "$source"'; 40 | } 41 | 42 | String substitute(String source, values, String encodeValue(value, String)) { 43 | 44 | var valueWriter; 45 | 46 | if (values is List) 47 | valueWriter = _createListValueWriter(values, encodeValue); 48 | 49 | else if (values is Map) 50 | valueWriter = _createMapValueWriter(values, encodeValue); 51 | 52 | else if (values == null) 53 | valueWriter = (_, _1, _2) 54 | => throw new ParseException('Template contains a parameter, but no values were passed.'); 55 | 56 | else 57 | throw new ArgumentError('Unexpected type.'); 58 | 59 | var buf = new StringBuffer(); 60 | var s = new _Scanner(source); 61 | 62 | while (s.hasMore()) { 63 | var t = s.read(); 64 | if (t.type == _TOKEN_IDENT) { 65 | valueWriter(buf, t.value, t.typeName); 66 | } else { 67 | buf.write(t.value); 68 | } 69 | } 70 | 71 | return buf.toString(); 72 | } 73 | 74 | _createListValueWriter(List list, String encodeValue(value, String)) 75 | => (StringSink buf, String identifier, String type) { 76 | 77 | int i = int.parse(identifier, onError: 78 | (_) => throw new ParseException('Expected integer parameter.')); 79 | 80 | if (i < 0 || i >= list.length) 81 | throw new ParseException('Substitution token out of range.'); 82 | 83 | var s = encodeValue(list[i], type); 84 | buf.write(s); 85 | }; 86 | 87 | _createMapValueWriter(Map map, String encodeValue(value, String)) 88 | => (StringSink buf, String identifier, String type) { 89 | 90 | var val; 91 | 92 | if (isDigit(identifier.codeUnits.first)) { 93 | int i = int.parse(identifier, onError: 94 | (_) => throw new ParseException('Expected integer parameter.')); 95 | 96 | if (i < 0 || i >= map.values.length) 97 | throw new ParseException("Substitution token out of range."); 98 | 99 | val = map.values.elementAt(i); 100 | 101 | } else { 102 | 103 | if (!map.keys.contains(identifier)) 104 | throw new ParseException("Substitution token not passed: $identifier."); 105 | 106 | val = map[identifier]; 107 | } 108 | 109 | var s = encodeValue(val, type); 110 | buf.write(s); 111 | }; 112 | 113 | class _Scanner { 114 | _Scanner(String source) 115 | : _source = source, 116 | _r = new _CharReader(source) { 117 | 118 | if (_r.hasMore()) 119 | _t = _read(); 120 | } 121 | 122 | final String _source; 123 | final _CharReader _r; 124 | _Token _t; 125 | 126 | bool hasMore() => _t != null; 127 | 128 | _Token peek() => _t; 129 | 130 | _Token read() { 131 | var t = _t; 132 | _t = _r.hasMore() ? _read() : null; 133 | return t; 134 | } 135 | 136 | _Token _read() { 137 | 138 | assert(_r.hasMore()); 139 | 140 | // '@@', '@ident', or '@ident:type' 141 | if (_r.peek() == _at) { 142 | _r.read(); 143 | 144 | if (!_r.hasMore()) 145 | throw new ParseException('Unexpected end of input.'); 146 | 147 | // Escaped '@' character. 148 | if (_r.peek() == _at) { 149 | _r.read(); 150 | return new _Token(_TOKEN_AT, '@'); 151 | } 152 | 153 | if (!isIdentifier(_r.peek())) 154 | throw new ParseException('Expected alphanumeric identifier character after "@".'); 155 | 156 | // Identifier 157 | var ident = _r.readWhile(isIdentifier); 158 | 159 | // Optional type modifier 160 | var type; 161 | if (_r.peek() == _colon) { 162 | _r.read(); 163 | type = _r.readWhile(isIdentifier); 164 | } 165 | return new _Token(_TOKEN_IDENT, ident, type); 166 | } 167 | 168 | // Read plain text 169 | var text = _r.readWhile((c) => c != _at); 170 | return new _Token(_TOKEN_TEXT, text); 171 | } 172 | } 173 | 174 | class _CharReader { 175 | _CharReader(String source) 176 | : _source = source, 177 | _itr = source.codeUnits.iterator { 178 | 179 | if (source == null) 180 | throw new ArgumentError('Source is null.'); 181 | 182 | _i = 0; 183 | 184 | if (source != '') { 185 | _itr.moveNext(); 186 | _c = _itr.current; 187 | } 188 | } 189 | 190 | String _source; 191 | Iterator _itr; 192 | int _i, _c; 193 | 194 | bool hasMore() => _i < _source.length; 195 | 196 | int read() { 197 | var c = _c; 198 | _itr.moveNext(); 199 | _i++; 200 | _c = _itr.current; 201 | return c; 202 | } 203 | 204 | int peek() => _c; 205 | 206 | String readWhile([bool test(int charCode)]) { 207 | 208 | if (!hasMore()) 209 | throw new ParseException('Unexpected end of input.', _source, _i); 210 | 211 | int start = _i; 212 | 213 | while (hasMore() && test(peek())) { 214 | read(); 215 | } 216 | 217 | int end = hasMore() ? _i : _source.length; 218 | return _source.substring(start, end); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: postgresql 2 | version: 0.3.4+1 3 | author: Greg Lowe 4 | description: Postgresql database driver. 5 | homepage: https://github.com/xxgreg/dart_postgresql 6 | environment: 7 | sdk: ">=0.8.10+6 <2.0.0" 8 | dependencies: 9 | crypto: ">=0.9.2 <3.0.0" 10 | convert: ">=1.0.0 <3.0.0" 11 | func: "^0.1.0" 12 | dev_dependencies: 13 | test: ">=0.12.0 <0.13.0" 14 | yaml: ">=0.9.0 <3.0.0" 15 | -------------------------------------------------------------------------------- /test/buffer_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:test/test.dart'; 3 | import 'package:postgresql/src/buffer.dart'; 4 | 5 | smiles(int i) { 6 | var sb = new StringBuffer(); 7 | for (int j = 0; j < i; j++) { 8 | sb.write('smiles ☺'); 9 | } 10 | return sb.toString(); 11 | } 12 | 13 | main() { 14 | test('Buffer', () { 15 | var buf = new Buffer((msg) => new Exception(msg)); 16 | 17 | var msg = new MessageBuffer(); 18 | for (int i = 1; i < 100; i++) { 19 | msg.addInt16(42); 20 | msg.addInt32(43); 21 | msg.addUtf8String(smiles(20)); 22 | 23 | var s = smiles(i); 24 | msg.addInt32(UTF8.encode(s).length); 25 | msg.addUtf8String(s); 26 | } 27 | 28 | // Slice up into lots of little lists and add them to the buffer. 29 | var b = msg.buffer; 30 | int i = 0; 31 | while (b.isNotEmpty) { 32 | i += 7; 33 | var bytes = (i % 30) + 1; 34 | if (b.length < bytes) { 35 | buf.append(b.toList()); 36 | break; 37 | } 38 | buf.append(b.take(bytes).toList()); 39 | b = b.skip(bytes); 40 | } 41 | expect(buf.bytesAvailable, equals(msg.buffer.length)); 42 | 43 | // Read back from the buffer and check that all is ok. 44 | for (int i = 1; i < 100; i++) { 45 | expect(buf.readInt16(), equals(42)); 46 | expect(buf.readInt32(), equals(43)); 47 | expect(buf.readUtf8String(100000), smiles(20)); 48 | 49 | var s = smiles(i); 50 | int len = UTF8.encode(s).length; 51 | expect(buf.readInt32(), equals(len)); 52 | expect(buf.readUtf8StringN(len), equals(s)); 53 | expect(buf.readByte(), equals(0)); // Zero padding byte for string. 54 | } 55 | }); 56 | } 57 | 58 | void addUtf8String(List buffer, String s) { 59 | buffer.addAll(UTF8.encode(s)); 60 | } 61 | 62 | void addInt16(List buffer, int i) { 63 | assert(i >= -32768 && i <= 32767); 64 | 65 | if (i < 0) 66 | i = 0x10000 + i; 67 | 68 | int a = (i >> 8) & 0x00FF; 69 | int b = i & 0x00FF; 70 | 71 | buffer.add(a); 72 | buffer.add(b); 73 | } 74 | 75 | void addInt32(List buffer, int i) { 76 | assert(i >= -2147483648 && i <= 2147483647); 77 | 78 | if (i < 0) 79 | i = 0x100000000 + i; 80 | 81 | int a = (i >> 24) & 0x000000FF; 82 | int b = (i >> 16) & 0x000000FF; 83 | int c = (i >> 8) & 0x000000FF; 84 | int d = i & 0x000000FF; 85 | 86 | buffer.add(a); 87 | buffer.add(b); 88 | buffer.add(c); 89 | buffer.add(d); 90 | } 91 | -------------------------------------------------------------------------------- /test/postgresql_mock_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:postgresql/postgresql.dart'; 3 | import 'package:postgresql/src/mock/mock.dart'; 4 | import 'package:postgresql/src/protocol.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | 8 | main() { 9 | 10 | mockLogger = print; 11 | 12 | test('testStartup with socket', 13 | () => MockServer.startSocketServer().then(testStartup)); 14 | 15 | test('testStartup with mock socket', () => testStartup(new MockServer())); 16 | } 17 | 18 | int PG_TEXT = 25; 19 | 20 | 21 | //TODO test which parses/generates a recorded db stream to test protocol matches spec. 22 | // Might mean that testing can be done at the message object level. 23 | // But is good test test things like socket errors. 24 | testStartup(MockServer server) async { 25 | 26 | var connecting = server.connect(); 27 | var backendStarting = server.waitForConnect(); 28 | 29 | var backend = await backendStarting; 30 | 31 | //TODO make mock socket server and mock server behave the same. 32 | if (server is MockSocketServerImpl) 33 | await backend.waitForClient(); 34 | 35 | expect(backend.received, equals([new Startup('testdb', 'testdb').encode()])); 36 | 37 | backend.clear(); 38 | backend.sendToClient(new AuthenticationRequest.ok().encode()); 39 | backend.sendToClient(new ReadyForQuery(TransactionStatus.none).encode()); 40 | 41 | var conn = await connecting; 42 | 43 | var sql = "select 'foo'"; 44 | Stream querying = conn.query(sql); 45 | 46 | await backend.waitForClient(); 47 | 48 | expect(backend.received, equals([new Query(sql).encode()]), verbose: true); 49 | backend.clear(); 50 | 51 | backend.sendToClient(new RowDescription([new Field('?', PG_TEXT)]).encode()); 52 | backend.sendToClient(new DataRow.fromStrings(['foo']).encode()); 53 | 54 | var row = null; 55 | await for (var r in querying) { 56 | row = r; 57 | 58 | expect(row, new isInstanceOf()); 59 | expect(row.toList().length, equals(1)); 60 | expect(row[0], equals('foo')); 61 | 62 | backend.sendToClient(new CommandComplete('SELECT 1').encode()); 63 | backend.sendToClient(new ReadyForQuery(TransactionStatus.none).encode()); 64 | } 65 | 66 | expect(row, isNotNull); 67 | 68 | conn.close(); 69 | 70 | // Async in server, but sync in mock. 71 | //TODO make getter on backend. isRealSocket 72 | if (server is MockSocketServerImpl) 73 | await backend.waitForClient(); 74 | 75 | expect(backend.received, equals([new Terminate().encode()])); 76 | expect(backend.isDestroyed, isTrue); 77 | 78 | server.stop(); 79 | } 80 | 81 | -------------------------------------------------------------------------------- /test/postgresql_pool_db_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:test/test.dart'; 3 | import 'package:matcher/matcher.dart'; 4 | import 'package:postgresql/pool.dart'; 5 | 6 | 7 | //_log(msg) => print(msg); 8 | 9 | _log(msg) {} 10 | 11 | int secsSince(DateTime time) => time == null 12 | ? null 13 | : new DateTime.now().difference(time).inSeconds; 14 | 15 | debug(Pool pool) { 16 | 17 | int total = pool.connections.length; 18 | 19 | int available = pool.connections.where((c) => c.state == PooledConnectionState.available).length; 20 | 21 | int inUse = pool.connections.where((c) => c.state == PooledConnectionState.inUse).length; 22 | 23 | int waiting = pool.waitQueueLength; 24 | 25 | int leaked = pool.connections.where((c) => c.isLeaked).length; 26 | 27 | int testing = pool.connections.where((c) => c.state == PooledConnectionState.testing).length; 28 | 29 | int connecting = pool.connections.where((c) => c.state == PooledConnectionState.connecting).length; 30 | 31 | print('pool: ${pool.state} total: $total available: $available in-use: $inUse testing: $testing connecting: $connecting waiting: $waiting leaked: $leaked'); 32 | pool.connections.forEach((c) => print('${c.name} ${c.state} ${c.connectionState} est: ${secsSince(c.established)} obt: ${secsSince(c.obtained)} rls: ${secsSince(c.released)} leaked: ${c.isLeaked}')); 33 | print(''); 34 | } 35 | 36 | Duration secs(int s) => new Duration(seconds: s); 37 | Duration millis(int ms) => new Duration(milliseconds: ms); 38 | 39 | main() { 40 | 41 | int slowQueries = 3; 42 | int testConnects = 1; 43 | var queryPeriod = secs(2); 44 | var stopAfter = secs(120); 45 | 46 | var pool = new Pool('postgresql://testdb:password@localhost:5433/testdb', 47 | connectionTimeout: secs(15), 48 | leakDetectionThreshold: secs(3), 49 | restartIfAllConnectionsLeaked: true) 50 | ..messages.listen((msg) => print('###$msg###')); 51 | 52 | int queryError = 0; 53 | int connectError = 0; 54 | int connectTimeout = 0; 55 | int queriesSent = 0; 56 | int queriesCompleted = 0; 57 | int slowQueriesSent = 0; 58 | int slowQueriesCompleted = 0; 59 | 60 | 61 | var loggerFunc = (Timer t) { 62 | print('queriesSent: $queriesSent queriesCompleted: $queriesCompleted slowSent: $slowQueriesSent slowCompleted: $slowQueriesCompleted connect timeouts: $connectTimeout queryError: $queryError connectError: $connectError '); 63 | debug(pool); 64 | }; 65 | 66 | var logger = new Timer.periodic(queryPeriod, loggerFunc); 67 | 68 | // test('Connect', () { 69 | // var pass = expectAsync(() {}); 70 | var pass = () {}; 71 | 72 | testConnect(_) { 73 | pool.connect().then((conn) { 74 | _log('connected ${conn.backendPid}'); 75 | queriesSent++; 76 | conn.query("select 'oi';").toList() 77 | .then((rows) { 78 | queriesCompleted++; 79 | expect(rows[0].toList(), equals(['oi'])); 80 | }) 81 | .catchError((err) { _log('Query error: $err'); queryError++; }) 82 | .whenComplete(() { 83 | _log('close ${conn.backendPid}'); 84 | conn.close(); 85 | }); 86 | }) 87 | .catchError((err) { 88 | if (err is TimeoutException) { 89 | //print(err); 90 | connectTimeout++; 91 | } else { 92 | print('Connect error: $err'); connectError++; 93 | } 94 | }); 95 | } 96 | 97 | slowQuery() { 98 | pool.connect().then((conn) { 99 | _log('slow connected ${conn.backendPid}'); 100 | slowQueriesSent++; 101 | conn.query("select generate_series (1, 100000);").toList() 102 | .then((rows) { 103 | slowQueriesCompleted++; 104 | expect(rows.length, 100000); 105 | }) 106 | .catchError((err) { _log('Query error: $err'); queryError++; }) 107 | .whenComplete(() { 108 | _log('slow close ${conn.backendPid}'); 109 | conn.close(); 110 | }); 111 | }) 112 | .catchError((err) { 113 | if (err is TimeoutException) { 114 | //print(err); 115 | connectTimeout++; 116 | } else { 117 | print('Connect error: $err'); connectError++; 118 | } 119 | }); 120 | } 121 | 122 | // Wait for initial connections to be made before starting 123 | var timer; 124 | pool.start().then((_) { 125 | timer = new Timer.periodic(queryPeriod, (_) { 126 | for (var i = 0; i < slowQueries; i++) 127 | slowQuery(); 128 | for (var i = 0; i < testConnects; i++) 129 | testConnect(null); 130 | }); 131 | }).catchError((err, st) { 132 | print('Error starting connection pool.'); 133 | print(err); 134 | print(st); 135 | }); 136 | 137 | // Burst of connections 138 | // new Future.delayed(secs(15), () { 139 | // print('####################### BURST! #########################'); 140 | // for (int i = 0; i < 30; i++) { 141 | // testConnect(null); 142 | // } 143 | // }); 144 | // 145 | // new Future.delayed(secs(30), () { 146 | // print('####################### BURST! #########################'); 147 | // for (int i = 0; i < 30; i++) { 148 | // testConnect(null); 149 | // } 150 | // }); 151 | // 152 | new Timer.periodic(secs(10), (t) { 153 | pool.connect(debugName: 'leak!'); 154 | }); 155 | 156 | 157 | new Future.delayed(stopAfter, () { 158 | print('stop'); 159 | if (timer != null) timer.cancel(); 160 | pool.stop().then((_) { logger.cancel(); loggerFunc(null); }); 161 | pass(); 162 | }); 163 | 164 | // }); 165 | 166 | } 167 | -------------------------------------------------------------------------------- /test/postgresql_pool_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:postgresql/constants.dart'; 3 | import 'package:postgresql/pool.dart'; 4 | import 'package:postgresql/postgresql.dart'; 5 | import 'package:postgresql/src/mock/mock.dart'; 6 | import 'package:postgresql/src/pool_impl.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | 10 | //_log(msg) => print(msg); 11 | _log(msg) { } 12 | 13 | main() { 14 | mockLogger = _log; 15 | 16 | test('Test pool', testPool); 17 | test('Test start timeout', testStartTimeout); 18 | test('Test connect timeout', testConnectTimeout); 19 | test('Test wait queue', testWaitQueue); 20 | test('Test empty pool', testEmptyPool); 21 | } 22 | 23 | PoolImpl createPool(PoolSettings settings) { 24 | return new PoolImpl(settings, null, mockConnectionFactory()); 25 | } 26 | 27 | expectState(PoolImpl pool, {int total, int available, int inUse}) { 28 | int ctotal = pool.connections.length; 29 | int cavailable = pool.connections 30 | .where((c) => c.state == PooledConnectionState.available).length; 31 | int cinUse = pool.connections 32 | .where((c) => c.state == PooledConnectionState.inUse).length; 33 | 34 | if (total != null) expect(ctotal, equals(total)); 35 | if (available != null) expect(cavailable, equals(available)); 36 | if (inUse != null) expect(cinUse, equals(inUse)); 37 | } 38 | 39 | Future testPool() async { 40 | var pool = createPool(new PoolSettings( 41 | databaseUri: 'postgresql://fakeuri', minConnections: 2)); 42 | 43 | var v = await pool.start(); 44 | expect(v, isNull); 45 | expectState(pool, total: 2, available: 2, inUse: 0); 46 | 47 | var c = await pool.connect(); 48 | expectState(pool, total: 2, available: 1, inUse: 1); 49 | 50 | c.close(); 51 | 52 | // Wait for next event loop. 53 | await new Future(() {}); 54 | expectState(pool, total: 2, available: 2, inUse: 0); 55 | 56 | var stopFuture = pool.stop(); 57 | await new Future(() {}); 58 | expect(pool.state, equals(stopping)); 59 | 60 | var v2 = await stopFuture; 61 | expect(v2, isNull); 62 | expect(pool.state, equals(stopped)); 63 | expectState(pool, total: 0, available: 0, inUse: 0); 64 | } 65 | 66 | 67 | Future testStartTimeout() async { 68 | var mockConnect = mockConnectionFactory( 69 | () => new Future.delayed(new Duration(seconds: 10))); 70 | 71 | var settings = new PoolSettings( 72 | databaseUri: 'postgresql://fakeuri', 73 | startTimeout: new Duration(seconds: 2), 74 | minConnections: 2); 75 | 76 | var pool = new PoolImpl(settings, null, mockConnect); 77 | 78 | try { 79 | expect(pool.connections, isEmpty); 80 | await pool.start(); 81 | fail('Pool started, but should have timed out.'); 82 | } catch (ex) { 83 | expect(ex, new isInstanceOf()); 84 | expect(ex.message, contains('timed out')); 85 | expect(pool.state, equals(startFailed)); 86 | } 87 | } 88 | 89 | 90 | Future testConnectTimeout() async { 91 | var settings = new PoolSettings( 92 | databaseUri: 'postgresql://fakeuri', 93 | minConnections: 2, 94 | maxConnections: 2, 95 | connectionTimeout: new Duration(seconds: 2)); 96 | 97 | var pool = createPool(settings); 98 | 99 | expect(pool.connections, isEmpty); 100 | 101 | var f = pool.start(); 102 | expect(pool.state, equals(initial)); 103 | //new Future.microtask(() => expect(pool.state, equals(starting))); 104 | 105 | var v = await f; 106 | expect(v, isNull); 107 | 108 | expect(pool.state, equals(running)); 109 | expect(pool.connections.length, equals(settings.minConnections)); 110 | expect(pool.connections.where((c) => c.state == available).length, 111 | equals(settings.minConnections)); 112 | 113 | // Obtain all of the connections from the pool. 114 | var c1 = await pool.connect(); 115 | var c2 = await pool.connect(); 116 | 117 | expect(pool.connections.where((c) => c.state == available).length, 0); 118 | 119 | try { 120 | // All connections are in use, this should timeout. 121 | await pool.connect(); 122 | fail('connect() should have timed out.'); 123 | } on PostgresqlException catch (ex) { 124 | expect(ex, new isInstanceOf()); 125 | expect(ex.message, contains('timeout')); 126 | expect(pool.state, equals(running)); 127 | } 128 | 129 | c1.close(); 130 | expect(c1.state, equals(closed)); 131 | 132 | var c3 = await pool.connect(); 133 | expect(c3.state, equals(idle)); 134 | 135 | c2.close(); 136 | c3.close(); 137 | 138 | expect(c1.state, equals(closed)); 139 | expect(c3.state, equals(closed)); 140 | 141 | expect(pool.connections.where((c) => c.state == available).length, 142 | equals(settings.minConnections)); 143 | 144 | } 145 | 146 | 147 | Future testWaitQueue() async { 148 | var settings = new PoolSettings( 149 | databaseUri: 'postgresql://fakeuri', 150 | minConnections: 2, 151 | maxConnections: 2); 152 | 153 | var pool = createPool(settings); 154 | 155 | expect(pool.connections, isEmpty); 156 | 157 | var v = await pool.start(); 158 | 159 | expect(v, isNull); 160 | expect(pool.connections.length, equals(2)); 161 | expect(pool.connections.where((c) => c.state == available).length, 162 | equals(2)); 163 | 164 | var c1 = await pool.connect(); 165 | var c2 = await pool.connect(); 166 | 167 | c1.query('mock timeout 5').toList().then((r) => c1.close()); 168 | c2.query('mock timeout 10').toList().then((r) => c2.close()); 169 | 170 | var conns = pool.connections; 171 | expect(conns.length, equals(2)); 172 | expect(conns.where((c) => c.state == available).length, equals(0)); 173 | expect(conns.where((c) => c.state == inUse).length, equals(2)); 174 | 175 | var c3 = await pool.connect(); 176 | 177 | expect(c3.state, equals(idle)); 178 | 179 | c3.close(); 180 | 181 | 182 | } 183 | 184 | 185 | Future testEmptyPool() async { 186 | var settings = new PoolSettings( 187 | databaseUri: 'postgresql://fakeuri', 188 | minConnections: 0, 189 | maxConnections: 2); 190 | 191 | var pool = createPool(settings); 192 | 193 | expect(pool.connections, isEmpty); 194 | 195 | var v = await pool.start(); 196 | 197 | expect(v, isNull); 198 | expect(pool.connections.length, equals(0)); 199 | 200 | var c1 = await pool.connect(); 201 | 202 | expect(c1.state, equals(idle)); 203 | 204 | c1.close(); 205 | } 206 | -------------------------------------------------------------------------------- /test/postgresql_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:postgresql/constants.dart'; 4 | import 'package:postgresql/postgresql.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:yaml/yaml.dart'; 7 | 8 | /** 9 | * Loads configuration from yaml file into [Settings]. 10 | */ 11 | Settings loadSettings() { 12 | var map = loadYaml(new File('test/test_config.yaml').readAsStringSync()); 13 | return new Settings.fromMap(map); 14 | } 15 | 16 | main() { 17 | String validUri = loadSettings().toUri(); 18 | 19 | group('Connect', () { 20 | test('Connect', () async { 21 | var c = await connect(validUri); 22 | c.close(); 23 | }); 24 | 25 | // Make travis happy by cheating. 26 | // test('Connect failure - incorrect password', () { 27 | // var map = loadSettings().toMap(); 28 | // map['password'] = 'WRONG'; 29 | // var uri = new Settings.fromMap(map).toUri(); 30 | 31 | // connect(uri).then((c) => throw new Exception('Should not be reached.'), 32 | // onError: expectAsync((err) { /* boom! */ })); 33 | // }); 34 | 35 | //Should fail with a message like:settings.toUri() 36 | //AsyncError: 'SocketIOException: OS Error: Connection refused, errno = 111' 37 | test('Connect failure - incorrect port', () { 38 | var map = loadSettings().toMap(); 39 | map['port'] = 9037; 40 | var uri = new Settings.fromMap(map).toUri(); 41 | 42 | connect(uri).then((c) => throw new Exception('Should not be reached.'), 43 | onError: expectAsync((err) {/* boom! */})); 44 | }); 45 | 46 | test('Connect failure - connect to http server', () { 47 | var uri = 'postgresql://user:pwd@google.com:80/database'; 48 | connect(uri).then((c) => throw new Exception('Should not be reached.'), 49 | onError: expectAsync((err) {/* boom! */})); 50 | }); 51 | }); 52 | 53 | group('Close', () { 54 | test('Close multiple times.', () { 55 | connect(validUri).then((conn) { 56 | conn.close(); 57 | conn.close(); 58 | new Future.delayed(new Duration(milliseconds: 20)).then((_) { 59 | conn.close(); 60 | }); 61 | }); 62 | }); 63 | 64 | test('Query on closed connection.', () { 65 | var cb = expectAsync((e) {}); 66 | connect(validUri).then((conn) { 67 | conn.close(); 68 | conn 69 | .query("select 'blah'") 70 | .toList() 71 | .then((_) => throw new Exception('Should not be reached.')) 72 | .catchError(cb); 73 | }); 74 | }); 75 | 76 | test('Execute on closed connection.', () { 77 | var cb = expectAsync((e) {}); 78 | connect(validUri).then((conn) { 79 | conn.close(); 80 | conn 81 | .execute("select 'blah'") 82 | .then((_) => throw new Exception('Should not be reached.')) 83 | .catchError(cb); 84 | }); 85 | }); 86 | }); 87 | 88 | group('Query', () { 89 | Connection conn; 90 | 91 | setUp(() { 92 | return connect(validUri).then((c) => conn = c); 93 | }); 94 | 95 | tearDown(() { 96 | if (conn != null) conn.close(); 97 | }); 98 | 99 | test('Invalid sql statement', () { 100 | conn 101 | .query('elect 1') 102 | .toList() 103 | .then((rows) => throw new Exception('Should not be reached.'), 104 | onError: expectAsync((err) {/* boom! */})); 105 | }); 106 | 107 | test('Null sql statement', () { 108 | conn 109 | .query(null) 110 | .toList() 111 | .then((rows) => throw new Exception('Should not be reached.'), 112 | onError: expectAsync((err) {/* boom! */})); 113 | }); 114 | 115 | test('Empty sql statement', () { 116 | conn 117 | .query('') 118 | .toList() 119 | .then((rows) => throw new Exception('Should not be reached.'), 120 | onError: expectAsync((err) {/* boom! */})); 121 | }); 122 | 123 | test('Whitespace only sql statement', () async { 124 | var rows = await conn.query(' ').toList(); 125 | expect(rows.length, 0); 126 | }); 127 | 128 | test('Empty multi-statement', () async { 129 | var rows = await conn.query(''' 130 | select 'bob'; 131 | ; 132 | select 'jim'; 133 | ''').toList(); 134 | expect(rows.length, 2); 135 | }); 136 | 137 | test('Query queueing', () async { 138 | var rows = await conn.query('select 1').toList(); 139 | expect(rows[0][0], equals(1)); 140 | 141 | rows = await conn.query('select 2').toList(); 142 | expect(rows[0][0], equals(2)); 143 | 144 | rows = await conn.query('select 3').toList(); 145 | expect(rows[0][0], equals(3)); 146 | }); 147 | 148 | test('Query queueing with error', () async { 149 | try { 150 | await conn.query('elect 1').toList(); 151 | fail('Should not be reached'); 152 | } catch (err) {} 153 | 154 | var rows = await conn.query('select 2').toList(); 155 | expect(rows[0][0], equals(2)); 156 | 157 | rows = await conn.query('select 3').toList(); 158 | expect(rows[0][0], equals(3)); 159 | }); 160 | 161 | test('Multiple queries in a single sql statement', () async { 162 | var rows = await conn.query('select 1; select 2; select 3;').toList(); 163 | expect(rows[0][0], equals(1)); 164 | expect(rows[1][0], equals(2)); 165 | expect(rows[2][0], equals(3)); 166 | }); 167 | 168 | test('Substitution', () async { 169 | var rows = await conn.query( 170 | 'select @num, @num:text, @num:real, ' 171 | '@int, @int:text, @int:int, ' 172 | '@string, ' 173 | '@datetime, @datetime:date, @datetime:timestamp, ' 174 | '@boolean, @boolean_false, @boolean_null', 175 | { 176 | 'num': 1.2, 177 | 'int': 3, 178 | 'string': 'bob\njim', 179 | 'datetime': new DateTime(2013, 1, 1), 180 | 'boolean': true, 181 | 'boolean_false': false, 182 | 'boolean_null': null, 183 | }).toList(); 184 | expect(rows.length, 1); 185 | }); 186 | }); 187 | 188 | group('Data types', () { 189 | Connection conn; 190 | 191 | setUp(() { 192 | return connect(validUri, timeZone: 'UTC').then((c) => conn = c); 193 | }); 194 | 195 | tearDown(() { 196 | if (conn != null) conn.close(); 197 | }); 198 | 199 | test('Select String', () async { 200 | var rows = await conn.query("select 'blah'").toList(); 201 | expect(rows[0][0], equals('blah')); 202 | }); 203 | 204 | // Postgresql database doesn't allow null bytes in strings. 205 | test('Select String with null character.', () { 206 | conn 207 | .query("select '(\u0000)'") 208 | .toList() 209 | .then((r) => fail('Expected query failure.')) 210 | .catchError(expectAsync((e) => expect(e, isException))); 211 | }); 212 | 213 | test('Select UTF8 String', () async { 214 | var list = await conn.query("select '☺'").toList(); 215 | expect(list[0][0], equals('☺')); 216 | }); 217 | 218 | test('Select int', () async { 219 | var rows = await conn.query('select 1, -1').toList(); 220 | expect(rows[0][0], equals(1)); 221 | expect(rows[0][1], equals(-1)); 222 | }); 223 | 224 | //FIXME Decimals not implemented yet. 225 | test('Select number', () async { 226 | var rows = await conn.query('select 1.1').toList(); 227 | expect(rows[0][0], equals('1.1')); 228 | }); 229 | 230 | test('Select boolean', () async { 231 | var rows = await conn.query('select true, false').toList(); 232 | expect(rows[0][0], equals(true)); 233 | expect(rows[0][1], equals(false)); 234 | }); 235 | 236 | test('Select null', () async { 237 | var rows = await conn.query('select null').toList(); 238 | expect(rows[0][0], equals(null)); 239 | }); 240 | 241 | test('Select int 2', () async { 242 | conn.execute( 243 | 'create temporary table dart_unit_test (a int2, b int4, c int8)'); 244 | conn.execute('insert into dart_unit_test values (1, 2, 3)'); 245 | conn.execute('insert into dart_unit_test values (-1, -2, -3)'); 246 | conn.execute('insert into dart_unit_test values (null, null, null)'); 247 | 248 | var rows = 249 | await conn.query('select a, b, c from dart_unit_test').toList(); 250 | expect(rows[0][0], equals(1)); 251 | expect(rows[0][1], equals(2)); 252 | expect(rows[0][2], equals(3)); 253 | 254 | expect(rows[1][0], equals(-1)); 255 | expect(rows[1][1], equals(-2)); 256 | expect(rows[1][2], equals(-3)); 257 | 258 | expect(rows[2][0], equals(null)); 259 | expect(rows[2][1], equals(null)); 260 | expect(rows[2][2], equals(null)); 261 | }); 262 | 263 | test('Select timestamp with milliseconds', () async { 264 | var t0 = new DateTime.utc(1979, 12, 20, 9, 0, 0, 0); 265 | var t1 = new DateTime.utc(1979, 12, 20, 9, 0, 0, 9); 266 | var t2 = new DateTime.utc(1979, 12, 20, 9, 0, 0, 10); 267 | var t3 = new DateTime.utc(1979, 12, 20, 9, 0, 0, 99); 268 | var t4 = new DateTime.utc(1979, 12, 20, 9, 0, 0, 100); 269 | var t5 = new DateTime.utc(1979, 12, 20, 9, 0, 0, 999); 270 | 271 | conn.execute('create temporary table dart_unit_test (a timestamp)'); 272 | 273 | var insert = 'insert into dart_unit_test values (@time)'; 274 | conn.execute(insert, {"time": t0}); 275 | conn.execute(insert, {"time": t1}); 276 | conn.execute(insert, {"time": t2}); 277 | conn.execute(insert, {"time": t3}); 278 | conn.execute(insert, {"time": t4}); 279 | conn.execute(insert, {"time": t5}); 280 | 281 | var rows = await conn 282 | .query('select a from dart_unit_test order by a asc') 283 | .toList(); 284 | expect((rows[0][0] as DateTime).difference(t0), Duration.ZERO); 285 | expect((rows[1][0] as DateTime).difference(t1), Duration.ZERO); 286 | expect((rows[2][0] as DateTime).difference(t2), Duration.ZERO); 287 | expect((rows[3][0] as DateTime).difference(t3), Duration.ZERO); 288 | expect((rows[4][0] as DateTime).difference(t4), Duration.ZERO); 289 | expect((rows[5][0] as DateTime).difference(t5), Duration.ZERO); 290 | }); 291 | 292 | test("Insert timestamp with milliseconds and timezone", () async { 293 | var t0 = new DateTime.now(); 294 | 295 | conn.execute('create temporary table dart_unit_test (a timestamptz)'); 296 | 297 | conn.execute("insert into dart_unit_test values (@time)", {"time": t0}); 298 | 299 | var rows = await conn.query("select a from dart_unit_test").toList(); 300 | expect((rows[0][0] as DateTime).difference(t0), Duration.ZERO); 301 | }); 302 | 303 | test( 304 | "Insert and select timestamp and timestamptz from using UTC and local DateTime", 305 | () async { 306 | var localNow = new DateTime.now(); 307 | var utcNow = new DateTime.now().toUtc(); 308 | 309 | conn.execute( 310 | 'create temporary table dart_unit_test (a timestamp, b timestamptz)'); 311 | conn.execute( 312 | "insert into dart_unit_test values (@timestamp, @timestamptz)", 313 | {"timestamp": utcNow, "timestamptz": localNow}); 314 | 315 | var rows = await conn.query("select a, b from dart_unit_test").toList(); 316 | expect((rows[0][0] as DateTime).difference(utcNow), Duration.ZERO, 317 | reason: "UTC -> Timestamp not the same"); 318 | expect((rows[0][1] as DateTime).difference(localNow), Duration.ZERO, 319 | reason: "Local -> Timestamptz not the same"); 320 | }); 321 | 322 | test("Multiple Timezone encoding/decoding", () async { 323 | var bdayGMT = DateTime.parse("1983-11-06T06:01:00.123+00:00"); 324 | var bdayPositiveTZ = DateTime.parse("1983-11-06T06:01:00.123+04:00"); 325 | var bdayNegativeTZ = DateTime.parse("1983-11-06T06:01:00.123-04:00"); 326 | var bdayPositiveMinuteTZ = 327 | DateTime.parse("1983-11-06T06:01:00.123+01:30"); 328 | var bdayNegativeMinuteTZ = 329 | DateTime.parse("1983-11-06T06:01:00.123-01:15"); 330 | 331 | conn.execute('create temporary table dart_unit_test (a timestamptz)'); 332 | conn.execute("insert into dart_unit_test values (@a)", {"a": bdayGMT}); 333 | conn.execute( 334 | "insert into dart_unit_test values (@a)", {"a": bdayPositiveTZ}); 335 | conn.execute( 336 | "insert into dart_unit_test values (@a)", {"a": bdayNegativeTZ}); 337 | conn.execute("insert into dart_unit_test values (@a)", 338 | {"a": bdayPositiveMinuteTZ}); 339 | conn.execute("insert into dart_unit_test values (@a)", 340 | {"a": bdayNegativeMinuteTZ}); 341 | 342 | var rows = await conn.query("select a from dart_unit_test").toList(); 343 | expect((rows[0][0] as DateTime).difference(bdayGMT), Duration.ZERO, 344 | reason: "GMT"); 345 | expect((rows[1][0] as DateTime).difference(bdayPositiveTZ), Duration.ZERO, 346 | reason: "Positive Hour TZ"); 347 | expect((rows[2][0] as DateTime).difference(bdayNegativeTZ), Duration.ZERO, 348 | reason: "Negative Hour TZ"); 349 | expect((rows[3][0] as DateTime).difference(bdayPositiveMinuteTZ), 350 | Duration.ZERO, 351 | reason: "Positive Minute TZ"); 352 | expect((rows[4][0] as DateTime).difference(bdayNegativeMinuteTZ), 353 | Duration.ZERO, 354 | reason: "Negative Minute TZ"); 355 | }); 356 | 357 | test("Selected null timestamp DateTime", () async { 358 | var dt = null; 359 | 360 | conn.execute('create temporary table dart_unit_test (a timestamp)'); 361 | conn.execute("insert into dart_unit_test values (@time)", {"time": dt}); 362 | 363 | var rows = await conn.query('select a from dart_unit_test').toList(); 364 | expect(rows[0], isNotNull); 365 | expect(rows[0][0], isNull); 366 | }); 367 | 368 | test('Select DateTime', () async { 369 | conn.execute('create temporary table dart_unit_test (a date)'); 370 | conn.execute("insert into dart_unit_test values ('1979-12-20')"); 371 | 372 | var rows = await conn.query('select a from dart_unit_test').toList(); 373 | expect(rows[0][0], equals(new DateTime.utc(1979, 12, 20))); 374 | }); 375 | 376 | // These tests will handle timestamps outside of the explicitly supported ISO8601 range. 377 | 378 | test("DateTime Edge Cases", () async { 379 | conn.execute('create temporary table dart_unit_test (a timestamp)'); 380 | conn.execute("insert into dart_unit_test (a) values (@t)", 381 | {"t": new DateTime(10000).toUtc()}); 382 | conn.execute("insert into dart_unit_test (a) values (@t)", 383 | {"t": new DateTime(-10).toUtc()}); 384 | 385 | var rows = await conn.query("select a from dart_unit_test").toList(); 386 | expect((rows[0][0] as DateTime).difference(new DateTime(10000).toUtc()), 387 | Duration.ZERO); 388 | expect((rows[1][0] as DateTime).difference(new DateTime(-10).toUtc()), 389 | Duration.ZERO); 390 | }); 391 | 392 | test('Select double', () async { 393 | conn.execute( 394 | 'create temporary table dart_unit_test (a float4, b float8)'); 395 | conn.execute("insert into dart_unit_test values (1.1, 2.2)"); 396 | conn.execute("insert into dart_unit_test values " 397 | "(-0.0, -0.0), ('NaN', 'NaN'), ('Infinity', 'Infinity'), " 398 | "('-Infinity', '-Infinity');"); 399 | conn.execute( 400 | "insert into dart_unit_test values " 401 | "(@0, @0), (@1, @1), (@2, @2), (@3, @3), (@4, @4), (@5, @5);", 402 | [ 403 | -0.0, 404 | double.NAN, 405 | double.INFINITY, 406 | double.NEGATIVE_INFINITY, 407 | 1e30, 408 | 1e-30 409 | ]); 410 | 411 | var rows = await conn.query('select a, b from dart_unit_test').toList(); 412 | expect(rows[0][0], equals(1.1.toDouble())); 413 | expect(rows[0][1], equals(2.2.toDouble())); 414 | 415 | expect(rows[1][0], equals(-0.0)); 416 | expect(rows[1][1], equals(-0.0)); 417 | 418 | expect(rows[2][0], isNaN); 419 | expect(rows[2][1], isNaN); 420 | 421 | expect(rows[3][0], equals(double.INFINITY)); 422 | expect(rows[3][1], equals(double.INFINITY)); 423 | 424 | expect(rows[4][0], equals(double.NEGATIVE_INFINITY)); 425 | expect(rows[4][1], equals(double.NEGATIVE_INFINITY)); 426 | 427 | expect(rows[5][0], equals(-0.0)); 428 | expect(rows[5][1], equals(-0.0)); 429 | 430 | expect(rows[6][0], isNaN); 431 | expect(rows[6][1], isNaN); 432 | 433 | expect(rows[7][0], equals(double.INFINITY)); 434 | expect(rows[7][1], equals(double.INFINITY)); 435 | 436 | expect(rows[8][0], equals(double.NEGATIVE_INFINITY)); 437 | expect(rows[8][1], equals(double.NEGATIVE_INFINITY)); 438 | 439 | expect(rows[9][0], equals(1e30)); 440 | expect(rows[9][1], equals(1e30)); 441 | 442 | expect(rows[10][0], equals(1e-30)); 443 | expect(rows[10][1], equals(1e-30)); 444 | }); 445 | 446 | //TODO 447 | // numeric (Need a BigDecimal type). 448 | // time 449 | // interval 450 | // timestamp and date with a timezone offset. 451 | }); 452 | 453 | group('Execute', () { 454 | Connection conn; 455 | 456 | setUp(() { 457 | return connect(validUri).then((c) => conn = c); 458 | }); 459 | 460 | tearDown(() { 461 | if (conn != null) conn.close(); 462 | }); 463 | 464 | test('Rows affected', () async { 465 | conn.execute('create temporary table dart_unit_test (a int)'); 466 | 467 | int rowsAffected = 468 | await conn.execute('insert into dart_unit_test values (1), (2), (3)'); 469 | expect(rowsAffected, equals(3)); 470 | 471 | rowsAffected = 472 | await conn.execute('update dart_unit_test set a = 5 where a = 1'); 473 | expect(rowsAffected, equals(1)); 474 | rowsAffected = 475 | await conn.execute('delete from dart_unit_test where a > 2'); 476 | expect(rowsAffected, equals(2)); 477 | rowsAffected = await conn.execute('create temporary table bob (a int)'); 478 | expect(rowsAffected, equals(null)); 479 | rowsAffected = await conn.execute(''' 480 | select 'one'; 481 | create temporary table jim (a int); 482 | create temporary table sally (a int); 483 | '''); 484 | expect(rowsAffected, equals(null)); 485 | }); 486 | }); 487 | 488 | group('PgException', () { 489 | Connection conn; 490 | 491 | setUp(() { 492 | return connect(validUri).then((c) => conn = c); 493 | }); 494 | 495 | tearDown(() { 496 | if (conn != null) conn.close(); 497 | }); 498 | 499 | // This test depends on the locale settings of the postgresql server. 500 | test('Error information for invalid sql statement', () { 501 | conn 502 | .query('elect 1') 503 | .toList() 504 | .then((rows) => throw new Exception('Should not be reached.'), 505 | onError: expectAsync((err) { 506 | expect(err, new isInstanceOf()); 507 | expect(err.serverMessage, isNotNull); 508 | expect(err.serverMessage.severity, equals('ERROR')); 509 | expect(err.serverMessage.code, equals('42601')); 510 | expect(err.serverMessage.position, equals("1")); 511 | })); 512 | }); 513 | }); 514 | 515 | group('Object mapping', () { 516 | Connection conn; 517 | 518 | setUp(() { 519 | return connect(validUri).then((c) => conn = c); 520 | }); 521 | 522 | tearDown(() { 523 | if (conn != null) conn.close(); 524 | }); 525 | 526 | test('Map person.', () async { 527 | var result = await conn 528 | .query(''' 529 | select 'Greg' as firstname, 'Lowe' as lastname; 530 | select 'Bob' as firstname, 'Jones' as lastname; 531 | ''') 532 | .map((row) => new Person() 533 | ..firstname = row.firstname 534 | ..lastname = row.lastname) 535 | .toList(); 536 | expect(result.last.lastname, 'Jones'); 537 | }); 538 | 539 | test('Map person immutable.', () async { 540 | var result = await conn 541 | .query(''' 542 | select 'Greg' as firstname, 'Lowe' as lastname; 543 | select 'Bob' as firstname, 'Jones' as lastname; 544 | ''') 545 | .map((row) => new ImmutablePerson(row.firstname, row.lastname)) 546 | .toList(); 547 | expect(result.last.lastname, 'Jones'); 548 | }); 549 | }); 550 | 551 | group('Transactions', () { 552 | Connection conn1; 553 | Connection conn2; 554 | 555 | setUp(() { 556 | return connect(validUri) 557 | .then((c) => conn1 = c) 558 | .then((_) => connect(validUri)) 559 | .then((c) => conn2 = c) 560 | .then((_) => conn1.execute( 561 | 'create table if not exists tx (val int); delete from tx;')); // if not exists requires pg9.1 562 | }); 563 | 564 | tearDown(() { 565 | if (conn1 != null) conn1.close(); 566 | if (conn2 != null) conn2.close(); 567 | }); 568 | 569 | test('simple query', () { 570 | var cb = expectAsync((_) {}); 571 | conn1.runInTransaction(() { 572 | return conn1.query("select 'oi'").toList().then((result) { 573 | expect(result[0][0], equals('oi')); 574 | }); 575 | }).then(cb); 576 | }); 577 | 578 | test('simple query read committed', () { 579 | var cb = expectAsync((_) {}); 580 | conn1.runInTransaction(() { 581 | return conn1.query("select 'oi'").toList().then((result) { 582 | expect(result[0][0], equals('oi')); 583 | }); 584 | }, readCommitted).then(cb); 585 | }); 586 | 587 | test('simple query repeatable read', () { 588 | var cb = expectAsync((_) {}); 589 | conn1.runInTransaction(() { 590 | return conn1.query("select 'oi'").toList().then((result) { 591 | expect(result[0][0], equals('oi')); 592 | }); 593 | }, readCommitted).then(cb); 594 | }); 595 | 596 | test('simple query serializable', () { 597 | var cb = expectAsync((_) {}); 598 | conn1.runInTransaction(() { 599 | return conn1.query("select 'oi'").toList().then((result) { 600 | expect(result[0][0], equals('oi')); 601 | }); 602 | }, serializable).then(cb); 603 | }); 604 | 605 | test('rollback', () { 606 | var cb = expectAsync((_) {}); 607 | 608 | conn1 609 | .runInTransaction(() { 610 | return conn1 611 | .execute('insert into tx values (42)') 612 | .then((_) => conn1.query('select val from tx').toList()) 613 | .then((result) { 614 | expect(result[0][0], equals(42)); 615 | }).then((_) => throw new Exception('Boom!')); 616 | }) 617 | .catchError((e) => print('Ignore: $e')) 618 | .then((_) => conn1.query('select val from tx').toList()) 619 | .then((result) { 620 | expect(result, equals([])); 621 | }) 622 | .then(cb); 623 | }); 624 | 625 | test('type converter', () { 626 | connect(validUri, typeConverter: new TypeConverter.raw()).then((c) { 627 | c.query('select true, 42').toList().then((result) { 628 | expect(result[0][0], equals('t')); 629 | expect(result[0][1], equals('42')); 630 | c.close(); 631 | }); 632 | }); 633 | }); 634 | 635 | //TODO test Row.toList() and Row.toMap() 636 | 637 | test('getColumns', () { 638 | conn1.query('select 42 as val').toList().then(expectAsync((rows) { 639 | rows.forEach((row) { 640 | expect(row.getColumns()[0].name, 'val'); 641 | }); 642 | })); 643 | }); 644 | 645 | test('isolation', () { 646 | var cb = expectAsync((_) {}); 647 | conn1.runInTransaction(() async { 648 | int count = await conn1.execute('insert into tx values (42)'); 649 | expect(count, 1, reason: "single value should be added"); 650 | List result = await (await conn1.query('select val from tx')).toList(); 651 | expect(result[0][0], equals(42)); 652 | await conn2.runInTransaction(() async { 653 | await conn2.execute('insert into tx values (43)'); 654 | 655 | ///Read committed conn2 should not see 42 added by conn1 656 | List result2 = 657 | await (await conn2.query('select min(val) from tx')).toList(); 658 | expect(result2[0][0], equals(43)); 659 | }); 660 | }).then(cb); 661 | }); 662 | }); 663 | } 664 | 665 | class Person { 666 | String firstname; 667 | String lastname; 668 | String toString() => '$firstname $lastname'; 669 | } 670 | 671 | class ImmutablePerson { 672 | ImmutablePerson(this.firstname, this.lastname); 673 | final String firstname; 674 | final String lastname; 675 | String toString() => '$firstname $lastname'; 676 | } 677 | -------------------------------------------------------------------------------- /test/settings_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:postgresql/pool.dart'; 3 | import 'package:postgresql/postgresql.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:yaml/yaml.dart'; 6 | 7 | Settings loadSettings(){ 8 | var map = loadYaml(new File('test/test_config.yaml').readAsStringSync()); 9 | return new Settings.fromMap(map); 10 | } 11 | 12 | main() { 13 | 14 | test('Test missing user setting', () { 15 | expect(() => new Settings.fromMap( 16 | {'host': 'host', 'password': 'password', 'database': 'database'}), 17 | throwsA(predicate((e) => e is PostgresqlException))); 18 | }); 19 | 20 | test('Test missing password setting', () { 21 | expect(() => new Settings.fromMap( 22 | {'host': 'host', 'user': 'user', 'database': 'database'}), 23 | throwsA(predicate((e) => e is PostgresqlException))); 24 | }); 25 | 26 | test('Test missing database setting', () { 27 | expect(() => new Settings.fromMap( 28 | {'host': 'host', 'password': 'password', 'user': 'user'}), 29 | throwsA(predicate((e) => e is PostgresqlException))); 30 | }); 31 | 32 | test('Valid settings', () { 33 | expect(new Settings.fromMap( 34 | {'user': 'user', 'password': 'password', 'host': 'host', 35 | 'database': 'database'}).toUri(), 36 | equals('postgres://user:password@host:5432/database')); 37 | }); 38 | 39 | test('Valid settings - empty password', () { 40 | expect(new Settings.fromMap( 41 | {'user': 'user', 'password': '', 'host': 'host', 42 | 'database': 'database'}).toUri(), 43 | equals('postgres://user@host:5432/database')); 44 | 45 | expect(new Settings.fromMap( 46 | {'user': 'user', 'password': null, 'host': 'host', 47 | 'database': 'database'}).toUri(), 48 | equals('postgres://user@host:5432/database')); 49 | }); 50 | 51 | test('Valid Uri', () { 52 | expect(new Settings.fromUri('postgres://user:password@host:5433/database').toMap(), 53 | equals({'host': 'host', 'user': 'user', 'password': 'password', 54 | 'database': 'database', 'port': 5433})); 55 | 56 | expect(new Settings.fromUri('postgres://user:password@host/database').toMap(), 57 | equals({'host': 'host', 'user': 'user', 'password': 'password', 58 | 'database': 'database', 'port': 5432})); 59 | 60 | expect(new Settings.fromUri('postgres://user@host/database').toMap(), 61 | equals({'host': 'host', 'user': 'user', 'password': '', 62 | 'database': 'database', 'port': 5432})); 63 | }); 64 | 65 | test('Valid settings different port', () { 66 | expect(new Settings.fromMap({'host': 'host', 'user': 'user', 67 | 'password': 'password', 'database': 'database', 'port': 5433}).toUri(), 68 | equals('postgres://user:password@host:5433/database')); 69 | }); 70 | 71 | test('Missing password ok - from uri', () { 72 | expect(new Settings.fromUri('postgres://user@host/foo').password, 73 | equals('')); 74 | }); 75 | 76 | test('String encoding', () { 77 | var m = {'host': 'ho st', 78 | 'port': 5433, 79 | 'user': 'us er', 80 | 'password': 'pass word', 81 | 'database': 'data base'}; 82 | var uri = new Settings.fromMap(m).toUri(); 83 | expect(uri, equals('postgres://us%20er:pass%20word@ho%20st:5433/data%20base')); 84 | 85 | var settings = new Settings.fromUri(uri); 86 | expect(settings.toMap(), equals(m)); 87 | 88 | expect(new Settings.fromUri('postgres://us er:pass word@ho st:5433/data base').toUri().toString(), 89 | 'postgres://us%20er:pass%20word@ho%20st:5433/data%20base'); 90 | }); 91 | 92 | test('Load settings from yaml file', () { 93 | Settings s = loadSettings(); 94 | expect(s.database, isNotNull); 95 | }); 96 | 97 | test('Pool settings', () { 98 | var uri = 'postgres://user:password@host:5432/database'; 99 | var s = new PoolSettings(databaseUri: uri); 100 | expect(s.databaseUri, equals(uri)); 101 | var m = s.toMap(); 102 | expect(m['databaseUri'], equals(uri)); 103 | var s2 = new PoolSettings.fromMap(m); 104 | expect(s2.databaseUri, equals(uri)); 105 | }); 106 | 107 | test('Pool settings', () { 108 | var uri = 'postgres://user:password@host:5432/database'; 109 | var d = new Duration(seconds: 42); 110 | var s = new PoolSettings( 111 | databaseUri: uri, 112 | minConnections: 20, 113 | maxConnections: 20, 114 | connectionTimeout: d, 115 | establishTimeout: d, 116 | idleTimeout: d, 117 | leakDetectionThreshold: d, 118 | maxLifetime: d, 119 | poolName: "foo", 120 | restartIfAllConnectionsLeaked: false, 121 | startTimeout: d, 122 | stopTimeout: d, 123 | testConnections: false); 124 | 125 | _testPoolSetting(value, flatValue, getter, mapKey) { 126 | expect(getter(), equals(value)); 127 | var m = s.toMap(); 128 | expect(m[mapKey], equals(flatValue)); 129 | expect(getter(), equals(value)); 130 | } 131 | 132 | _testPoolSetting( 133 | 'postgres://user:password@host:5432/database', 134 | 'postgres://user:password@host:5432/database', 135 | () => s.databaseUri, 136 | 'databaseUri'); 137 | 138 | _testPoolSetting( 139 | 20, 140 | 20, 141 | () => s.minConnections, 142 | 'minConnections'); 143 | 144 | _testPoolSetting( 145 | d, 146 | '42s', 147 | () => s.connectionTimeout, 148 | 'connectionTimeout'); 149 | 150 | _testPoolSetting( 151 | d, 152 | '42s', 153 | () => s.establishTimeout, 154 | 'establishTimeout'); 155 | 156 | _testPoolSetting( 157 | d, 158 | '42s', 159 | () => s.idleTimeout, 160 | 'idleTimeout'); 161 | 162 | _testPoolSetting( 163 | d, 164 | '42s', 165 | () => s.leakDetectionThreshold, 166 | 'leakDetectionThreshold'); 167 | 168 | _testPoolSetting( 169 | d, 170 | '42s', 171 | () => s.maxLifetime, 172 | 'maxLifetime'); 173 | 174 | 175 | _testPoolSetting( 176 | d, 177 | '42s', 178 | () => s.startTimeout, 179 | 'startTimeout'); 180 | 181 | _testPoolSetting( 182 | d, 183 | '42s', 184 | () => s.stopTimeout, 185 | 'stopTimeout'); 186 | 187 | _testPoolSetting( 188 | 'foo', 189 | 'foo', 190 | () => s.poolName, 191 | 'poolName'); 192 | 193 | _testPoolSetting( 194 | false, 195 | false, 196 | () => s.restartIfAllConnectionsLeaked, 197 | 'restartIfAllConnectionsLeaked'); 198 | 199 | _testPoolSetting( 200 | false, 201 | false, 202 | () => s.testConnections, 203 | 'testConnections'); 204 | }); 205 | 206 | } 207 | 208 | -------------------------------------------------------------------------------- /test/substitute_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:test/test.dart'; 3 | import 'package:postgresql/postgresql.dart'; 4 | import 'package:postgresql/src/postgresql_impl/postgresql_impl.dart'; 5 | import 'package:postgresql/src/substitute.dart'; 6 | import 'package:yaml/yaml.dart'; 7 | 8 | Settings loadSettings(){ 9 | var map = loadYaml(new File('test/test_config.yaml').readAsStringSync()); 10 | return new Settings.fromMap(map); 11 | } 12 | 13 | main() { 14 | 15 | DefaultTypeConverter tc = new TypeConverter(); 16 | 17 | group('Substitute by id', () { 18 | test('Substitute 1', () { 19 | var result = substitute('@id', {'id': 20}, tc.encodeValue); 20 | expect(result, equals('20')); 21 | }); 22 | 23 | test('Substitute 2', () { 24 | var result = substitute('@id ', {'id': 20}, tc.encodeValue); 25 | expect(result, equals('20 ')); 26 | }); 27 | 28 | test('Substitute 3', () { 29 | var result = substitute(' @id ', {'id': 20}, tc.encodeValue); 30 | expect(result, equals(' 20 ')); 31 | }); 32 | 33 | test('Substitute 4', () { 34 | var result = substitute('@id@bob', {'id': 20, 'bob': 13}, tc.encodeValue); 35 | expect(result, equals('2013')); 36 | }); 37 | 38 | test('Substitute 5', () { 39 | var result = substitute('..@id..', {'id': 20}, tc.encodeValue); 40 | expect(result, equals('..20..')); 41 | }); 42 | 43 | test('Substitute 6', () { 44 | var result = substitute('...@id...', {'id': 20}, tc.encodeValue); 45 | expect(result, equals('...20...')); 46 | }); 47 | 48 | test('Substitute 7', () { 49 | var result = substitute('...@id.@bob...', {'id': 20, 'bob': 13}, tc.encodeValue); 50 | expect(result, equals('...20.13...')); 51 | }); 52 | 53 | test('Substitute 8', () { 54 | var result = substitute('...@id@bob', {'id': 20, 'bob': 13}, tc.encodeValue); 55 | expect(result, equals('...2013')); 56 | }); 57 | 58 | test('Substitute 9', () { 59 | var result = substitute('@id@bob...', {'id': 20, 'bob': 13}, tc.encodeValue); 60 | expect(result, equals('2013...')); 61 | }); 62 | 63 | test('Substitute 10', () { 64 | var result = substitute('@id:text', {'id': 20, 'bob': 13}, tc.encodeValue); 65 | expect(result, equals(" E'20' ")); 66 | }); 67 | 68 | test('Substitute 11', () { 69 | var result = substitute('@blah_blah', {'blah_blah': 20}, tc.encodeValue); 70 | expect(result, equals("20")); 71 | }); 72 | 73 | test('Substitute 12', () { 74 | var result = substitute('@_blah_blah', {'_blah_blah': 20}, tc.encodeValue); 75 | expect(result, equals("20")); 76 | }); 77 | 78 | test('Substitute 13', () { 79 | var result = substitute('@0 @1', ['foo', 42], tc.encodeValue); 80 | expect(result, equals(" E'foo' 42")); 81 | }); 82 | 83 | // test('Substitute 13', () { 84 | // var result = substitute('@apos', {'apos': "'"}); 85 | // //expect(result, equals("E'''")); 86 | // //print('oi'); 87 | // print(result); 88 | // }); 89 | }); 90 | 91 | } -------------------------------------------------------------------------------- /test/test_config.yaml: -------------------------------------------------------------------------------- 1 | host: localhost 2 | port: 5432 3 | user: testdb 4 | password: password 5 | database: testdb 6 | -------------------------------------------------------------------------------- /test/type_converter_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:test/test.dart'; 3 | import 'package:postgresql/postgresql.dart'; 4 | import 'package:postgresql/src/postgresql_impl/postgresql_impl.dart'; 5 | import 'package:yaml/yaml.dart'; 6 | 7 | Settings loadSettings(){ 8 | var map = loadYaml(new File('test/test_config.yaml').readAsStringSync()); 9 | return new Settings.fromMap(map); 10 | } 11 | 12 | main() { 13 | 14 | DefaultTypeConverter tc = new TypeConverter(); 15 | 16 | test('String escaping', () { 17 | expect(tc.encodeValue('bob', null), equals(" E'bob' ")); 18 | expect(tc.encodeValue('bo\nb', null), equals(r" E'bo\nb' ")); 19 | expect(tc.encodeValue('bo\rb', null), equals(r" E'bo\rb' ")); 20 | expect(tc.encodeValue(r'bo\b', null), equals(r" E'bo\\b' ")); 21 | 22 | expect(tc.encodeValue(r"'", null), equals(r" E'\'' ")); 23 | expect(tc.encodeValue(r" '' ", null), equals(r" E' \'\' ' ")); 24 | expect(tc.encodeValue(r"\''", null), equals(r" E'\\\'\'' ")); 25 | }); 26 | 27 | 28 | 29 | // Timezone offsets 30 | //FIXME check that timezone offsets match the current system timezone offset. 31 | // Example strings that postgres may send. 32 | // "2001-02-03 04:05:06.123-07" 33 | // "2001-02-03 04:05:06-07" 34 | // "2001-02-03 04:05:06-07:42" 35 | // "2001-02-03 04:05:06-07:30:09" 36 | // "2001-02-03 04:05:06+07" 37 | // "0010-02-03 04:05:06.123-07 BC" 38 | 39 | // Also consider that some Dart datetimes will not be able to be represented 40 | // in postgresql timestamps. i.e. pre 4713 BC or post 294276 AD. Perhaps just 41 | // send these dates and rely on the database to return an error. 42 | 43 | 44 | test('encode datetime', () { 45 | // Get users current timezone 46 | var tz = new DateTime(2001, 2, 3).timeZoneOffset; 47 | var tzoff = "${tz.isNegative ? '-' : '+'}" 48 | "${tz.inHours.toString().padLeft(2, '0')}" 49 | ":${(tz.inSeconds % 60).toString().padLeft(2, '0')}"; 50 | 51 | var data = [ 52 | "2001-02-03T00:00:00.000$tzoff", new DateTime(2001, DateTime.FEBRUARY, 3), 53 | "2001-02-03T04:05:06.000$tzoff", new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 54 | "2001-02-03T04:05:06.999$tzoff", new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), 55 | "0010-02-03T04:05:06.123$tzoff BC", new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), 56 | "0010-02-03T04:05:06.000$tzoff BC", new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 57 | "012345-02-03T04:05:06.000$tzoff BC", new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 58 | "012345-02-03T04:05:06.000$tzoff", new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0) 59 | ]; 60 | var tc = new TypeConverter(); 61 | for (int i = 0; i < data.length; i += 2) { 62 | expect(tc.encode(data[i + 1], null), equals("'${data[i]}'")); 63 | } 64 | }); 65 | 66 | test('encode date', () { 67 | var data = [ 68 | "2001-02-03", new DateTime(2001, DateTime.FEBRUARY, 3), 69 | "2001-02-03", new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 70 | "2001-02-03", new DateTime(2001, DateTime.FEBRUARY, 3, 4, 5, 6, 999), 71 | "0010-02-03 BC", new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 123), 72 | "0010-02-03 BC", new DateTime(-10, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 73 | "012345-02-03 BC", new DateTime(-12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 74 | "012345-02-03", new DateTime(12345, DateTime.FEBRUARY, 3, 4, 5, 6, 0), 75 | ]; 76 | var tc = new TypeConverter(); 77 | for (int i = 0; i < data.length; i += 2) { 78 | var str = data[i]; 79 | var dt = data[i + 1]; 80 | expect(tc.encode(dt, 'date'), equals("'$str'")); 81 | } 82 | }); 83 | 84 | test('encode double', () { 85 | var data = [ 86 | "'nan'", double.NAN, 87 | "'infinity'", double.INFINITY, 88 | "'-infinity'", double.NEGATIVE_INFINITY, 89 | "1.7976931348623157e+308", double.MAX_FINITE, 90 | "5e-324", double.MIN_POSITIVE, 91 | "-0.0", -0.0, 92 | "0.0", 0.0 93 | ]; 94 | var tc = new TypeConverter(); 95 | for (int i = 0; i < data.length; i += 2) { 96 | var str = data[i]; 97 | var dt = data[i + 1]; 98 | expect(tc.encode(dt, null), equals(str)); 99 | expect(tc.encode(dt, 'real'), equals(str)); 100 | expect(tc.encode(dt, 'double'), equals(str)); 101 | } 102 | 103 | expect(tc.encode(null, 'real'), equals('null')); 104 | }); 105 | 106 | test('encode int', () { 107 | var tc = new TypeConverter(); 108 | expect(() => tc.encode(double.NAN, 'integer'), throws); 109 | expect(() => tc.encode(double.INFINITY, 'integer'), throws); 110 | expect(() => tc.encode(1.0, 'integer'), throws); 111 | 112 | expect(tc.encode(1, 'integer'), equals('1')); 113 | expect(tc.encode(1, null), equals('1')); 114 | 115 | expect(tc.encode(null, 'integer'), equals('null')); 116 | }); 117 | 118 | test('encode bool', () { 119 | var tc = new TypeConverter(); 120 | expect(tc.encode(null, 'bool'), equals('null')); 121 | expect(tc.encode(true, null), equals('true')); 122 | expect(tc.encode(false, null), equals('false')); 123 | expect(tc.encode(true, 'bool'), equals('true')); 124 | expect(tc.encode(false, 'bool'), equals('false')); 125 | }); 126 | 127 | test('encode json', () { 128 | var tc = new TypeConverter(); 129 | expect(tc.encode({"foo": "bar"}, 'json'), equals(' E\'{"foo":"bar"}\' ')); 130 | expect(tc.encode({"foo": "bar"}, null), equals(' E\'{"foo":"bar"}\' ')); 131 | expect(tc.encode({"fo'o": "ba'r"}, 'json'), equals(' E\'{"fo\\\'o":"ba\\\'r"}\' ')); 132 | }); 133 | 134 | test('0.2 compatability test.', () { 135 | var tc = new TypeConverter(); 136 | expect(tc.encode(null, null), equals('null')); 137 | expect(tc.encode(null, 'string'), equals('null')); 138 | expect(tc.encode(null, 'number'), equals('null')); 139 | expect(tc.encode(null, 'foo'), equals('null')); // Should this be an error?? 140 | 141 | expect(tc.encode(1, null), equals('1')); 142 | expect(tc.encode(1, 'number'), equals('1')); 143 | expect(tc.encode(1, 'string'), equals(" E'1' ")); 144 | expect(tc.encode(1, 'String'), equals(" E'1' ")); 145 | 146 | expect(tc.encode(new DateTime.utc(1979,12,20,9), 'date'), equals("'1979-12-20'")); 147 | expect(tc.encode(new DateTime.utc(1979,12,20,9), 'timestamp'), equals("'1979-12-20T09:00:00.000Z'")); 148 | }); 149 | 150 | 151 | //TODO test array 152 | //TODO test bytea 153 | } 154 | -------------------------------------------------------------------------------- /tool/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2014, Google Inc. Please see the AUTHORS file for details. 4 | # All rights reserved. Use of this source code is governed by a BSD-style 5 | # license that can be found in the LICENSE file. 6 | 7 | # Fast fail the script on failures. 8 | set -e 9 | 10 | # Get the Dart SDK (only for travis-ci.org; otherwise, assume that Dart is already available). 11 | if [ "$TRAVIS" = "true" ]; then 12 | DART_DIST=dartsdk-linux-x64-release.zip 13 | curl http://storage.googleapis.com/dart-archive/channels/stable/release/latest/sdk/$DART_DIST > $DART_DIST 14 | unzip $DART_DIST > /dev/null 15 | rm $DART_DIST 16 | export DART_SDK="$PWD/dart-sdk" 17 | export PATH="$DART_SDK/bin:$PATH" 18 | fi 19 | 20 | # Display installed versions. 21 | dart --version 22 | 23 | export PATH="$PATH":"~/.pub-cache/bin" 24 | 25 | # Get our packages. 26 | pub get 27 | 28 | # Run the analyzer and tests. 29 | ./check-all.sh 30 | 31 | # Gather and send coverage data. 32 | #if [ "$REPO_TOKEN" ]; then 33 | #pub global activate dart_coveralls 34 | #pub global run dart_coveralls report --token $REPO_TOKEN --retry 2 --exclude-test-files test/all.dart 35 | #fi 36 | --------------------------------------------------------------------------------