From 18946b432054e6d5f324abf78a5707d565fe125d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 14 Jul 2017 18:04:58 -0400 Subject: [PATCH] 1.0.0-alpha+2 --- angel_orm/CHANGELOG.md | 4 + angel_orm/lib/src/pool.dart | 24 +++++- angel_orm/lib/src/query.dart | 146 +++++++++++++++++++++++++++++++++-- angel_orm/pubspec.yaml | 6 +- 4 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 angel_orm/CHANGELOG.md diff --git a/angel_orm/CHANGELOG.md b/angel_orm/CHANGELOG.md new file mode 100644 index 00000000..431320d3 --- /dev/null +++ b/angel_orm/CHANGELOG.md @@ -0,0 +1,4 @@ +# 1.0.0-alpha+2 +* Added `isIn`, `isNotIn`, `isBetween`, `isNotBetween` to `SqlExpressionBuilder` and its +subclasses. +* Added a dependency on `package:meta`. \ No newline at end of file diff --git a/angel_orm/lib/src/pool.dart b/angel_orm/lib/src/pool.dart index bc870bf5..9cdb0722 100644 --- a/angel_orm/lib/src/pool.dart +++ b/angel_orm/lib/src/pool.dart @@ -7,6 +7,9 @@ typedef FutureOr PostgreSQLConnector(); /// Pools connections to a PostgreSQL database. class PostgreSQLConnectionPool { + final List _connections = []; + final List _opened = []; + int _index = 0; Pool _pool; /// The maximum number of concurrent connections to the database. @@ -26,11 +29,22 @@ class PostgreSQLConnectionPool { } Future _connect() async { - var connection = await connector() as PostgreSQLConnection; - await connection.open(); + if (_connections.isEmpty) { + for (int i = 0; i < concurrency; i++) { + _connections.add(await connector()); + } + } + + var connection = _connections[_index++]; + if (_index >= _connections.length) _index = 0; + + if (!_opened.contains(connection.hashCode)) await connection.open(); + return connection; } + Future close() => Future.wait(_connections.map((c) => c.close())); + /// Connects to the database, and then executes the [callback]. /// /// Returns the result of [callback]. @@ -39,7 +53,11 @@ class PostgreSQLConnectionPool { return _connect().then((connection) { return new Future.sync(() => callback(connection)) .whenComplete(() async { - if (!connection.isClosed) await connection.close(); + if (connection.isClosed) { + _connections + ..remove(connection) + ..add(await connector()); + } resx.release(); }); }); diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index 7d114f5a..603d8385 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -1,23 +1,54 @@ +import 'package:charcode/charcode.dart'; +import 'package:meta/meta.dart'; import 'package:intl/intl.dart'; +import 'package:string_scanner/string_scanner.dart'; final DateFormat DATE_YMD = new DateFormat('yyyy-MM-dd'); final DateFormat DATE_YMD_HMS = new DateFormat('yyyy-MM-dd HH:mm:ss'); +/// Cleans an input SQL expression of common SQL injection points. +String sanitizeExpression(String unsafe) { + var buf = new StringBuffer(); + var scanner = new StringScanner(unsafe); + int ch; + + while (!scanner.isDone) { + // Ignore comment starts + if (scanner.scan('--') || scanner.scan('/*')) + continue; + + // Ignore all single quotes and attempted escape sequences + else if (scanner.scan("'") || scanner.scan('\\')) + continue; + + // Otherwise, add the next char, unless it's a null byte. + else if ((ch == scanner.readChar()) != 0) buf.writeCharCode(ch); + } + + return buf.toString(); +} + abstract class SqlExpressionBuilder { bool get hasValue; String compile(); + void isBetween(lower, upper); + void isNotBetween(lower, upper); + void isIn(Iterable values); + void isNotIn(Iterable values); } class NumericSqlExpressionBuilder implements SqlExpressionBuilder { bool _hasValue = false; String _op = '='; + String _raw; T _value; @override bool get hasValue => _hasValue; bool _change(String op, T value) { + _raw = null; _op = op; _value = value; return _hasValue = true; @@ -25,6 +56,7 @@ class NumericSqlExpressionBuilder @override String compile() { + if (_raw != null) return _raw; if (_value == null) return null; return '$_op $_value'; } @@ -57,18 +89,41 @@ class NumericSqlExpressionBuilder void notEquals(T value) { _change('!=', value); } + + @override + void isBetween(@checked T lower, @checked T upper) { + _raw = 'BETWEEN $lower AND $upper'; + _hasValue = true; + } + + @override + void isNotBetween(@checked T lower, @checked T upper) { + _raw = 'NOT BETWEEN $lower AND $upper'; + _hasValue = true; + } + + @override + void isIn(@checked Iterable values) { + _raw = 'IN (' + values.join(', ') + ')'; + _hasValue = true; + } + + @override + void isNotIn(@checked Iterable values) { + _raw = 'NOT IN (' + values.join(', ') + ')'; + _hasValue = true; + } } -// TODO: Escape SQL Strings class StringSqlExpressionBuilder implements SqlExpressionBuilder { bool _hasValue = false; - String _op = '='; - String _value; + String _op = '=', _raw, _value; @override bool get hasValue => _hasValue; bool _change(String op, String value) { + _raw = null; _op = op; _value = value; return _hasValue = true; @@ -76,8 +131,9 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder { @override String compile() { + if (_raw != null) return _raw; if (_value == null) return null; - var v = _value.replaceAll("'", "\\'"); + var v = sanitizeExpression(_value); return "$_op '$v'"; } @@ -94,17 +150,48 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder { void like(String value) { _change('LIKE', value); } + + @override + void isBetween(@checked String lower, @checked String upper) { + var l = sanitizeExpression(lower), u = sanitizeExpression(upper); + _raw = "BETWEEN '$l' AND '$u'"; + _hasValue = true; + } + + @override + void isNotBetween(@checked String lower, @checked String upper) { + var l = sanitizeExpression(lower), u = sanitizeExpression(upper); + _raw = "NOT BETWEEN '$l' AND '$u'"; + _hasValue = true; + } + + @override + void isIn(@checked Iterable values) { + _raw = 'IN (' + + values.map(sanitizeExpression).map((s) => "'$s'").join(', ') + + ')'; + _hasValue = true; + } + + @override + void isNotIn(@checked Iterable values) { + _raw = 'NOT IN (' + + values.map(sanitizeExpression).map((s) => "'$s'").join(', ') + + ')'; + _hasValue = true; + } } class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { bool _hasValue = false; - String _op = '='; + String _op = '=', _raw; bool _value; @override bool get hasValue => _hasValue; bool _change(String op, bool value) { + _raw = null; _op = op; _value = value; return _hasValue = true; @@ -112,6 +199,7 @@ class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { @override String compile() { + if (_raw != null) return _raw; if (_value == null) return null; var v = _value ? 'TRUE' : 'FALSE'; return '$_op $v'; @@ -124,6 +212,28 @@ class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { void notEquals(bool value) { _change('!=', value); } + + @override + void isBetween(@checked bool lower, @checked bool upper) => + throw new UnsupportedError( + 'Booleans do not support BETWEEN expressions.'); + + @override + void isNotBetween(@checked bool lower, @checked bool upper) => + isBetween(lower, upper); + + @override + void isIn(@checked Iterable values) { + _raw = 'IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')'; + _hasValue = true; + } + + @override + void isNotIn(@checked Iterable values) { + _raw = + 'NOT IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')'; + _hasValue = true; + } } class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { @@ -180,6 +290,32 @@ class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { _change('>=', value, includeTime != false); } + @override + void isIn(@checked Iterable values) { + _raw = '"$columnName" IN (' + + values.map(DATE_YMD_HMS.format).map((s) => "'$s'").join(', ') + + ')'; + } + + @override + void isNotIn(@checked Iterable values) { + _raw = '"$columnName" NOT IN (' + + values.map(DATE_YMD_HMS.format).map((s) => "'$s'").join(', ') + + ')'; + } + + @override + void isBetween(@checked DateTime lower, @checked DateTime upper) { + var l = DATE_YMD_HMS.format(lower), u = DATE_YMD_HMS.format(upper); + _raw = "\"$columnName\" BETWEEN '$l' and '$u'"; + } + + @override + void isNotBetween(@checked DateTime lower, @checked DateTime upper) { + var l = DATE_YMD_HMS.format(lower), u = DATE_YMD_HMS.format(upper); + _raw = "\"$columnName\" NOT BETWEEN '$l' and '$u'"; + } + @override String compile() { if (_raw?.isNotEmpty == true) return _raw; diff --git a/angel_orm/pubspec.yaml b/angel_orm/pubspec.yaml index 4aaab7ed..ff63866c 100644 --- a/angel_orm/pubspec.yaml +++ b/angel_orm/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_orm -version: 1.0.0-alpha+2 +version: 1.0.0-alpha+3 description: Runtime support for Angel's ORM. author: Tobe O homepage: https://github.com/angel-dart/orm @@ -7,5 +7,7 @@ environment: sdk: ">=1.19.0" dependencies: intl: ">=0.0.0 <1.0.0" + meta: ^1.0.0 pool: ^1.0.0 - postgres: ^0.9.5 \ No newline at end of file + postgres: ^0.9.5 + string_scanner: ^1.0.0 \ No newline at end of file