Remove Postgres from ORM, fluent query builder, example

This commit is contained in:
Tobe O 2018-05-03 23:51:17 -04:00
parent 8dff26a8c0
commit 9dd1dccf41
10 changed files with 159 additions and 512 deletions

View file

@ -1,3 +1,9 @@
# 1.0.0-alpha+11
* Removed PostgreSQL-specific functionality, so that the ORM can ultimately
target all services.
* Created a better `Join` model.
* Created a far better `Query` model.
# 1.0.0-alpha+10
* Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures.

View file

@ -1,7 +1,6 @@
# angel_orm
Runtime support for Angel's ORM. Includes SQL expression generators, as well
as a friendly `PostgreSQLConnectionPool` class that you can use to pool connections
to a PostgreSQL database.
Runtime support for Angel's ORM. Includes a clean, database-agnostic
query builder and relationship/join support.
For documentation about the ORM, head to the main project repo:
https://github.com/angel-dart/orm

View file

@ -0,0 +1,27 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
Query findEmployees(Company company) {
return new Query()
..['company_id'] = equals(company.id)
..['first_name'] = notNull() & (equals('John'))
..['salary'] = greaterThanOrEqual(100000.0);
}
@ORM('api/companies')
class Company extends Model {
String name;
bool isFortune500;
}
@orm
class Employee extends Model {
@belongsTo
Company company;
String firstName, lastName;
double salary;
bool get isFortune500Employee => company.isFortune500;
}

View file

@ -1,4 +1,3 @@
export 'src/annotations.dart';
export 'src/migration.dart';
export 'src/relations.dart';
export 'src/query.dart';
export 'src/relations.dart';

View file

@ -1,12 +1,31 @@
const ORM orm = const ORM();
class ORM {
final String tableName;
const ORM([this.tableName]);
/// The path to an Angel service that queries objects of the
/// annotated type at runtime.
///
/// Ex. `api/users`, etc.
final String servicePath;
const ORM([this.servicePath]);
}
class CanJoin {
/// Specifies that the ORM should build a join builder
/// that combines the results of queries on two services.
class Join {
/// The [Model] type to join against.
final Type type;
final String foreignKey;
const CanJoin(this.type, this.foreignKey);
/// The path to an Angel service that queries objects of the
/// [type] being joined against, at runtime.
///
/// Ex. `api/users`, etc.
final String servicePath;
/// The type of join this is.
final JoinType joinType;
const Join(this.type, this.servicePath, [this.joinType = JoinType.join]);
}
/// The various types of [Join].
enum JoinType { join, left, right, full, self }

View file

@ -1,113 +0,0 @@
const List<String> SQL_RESERVED_WORDS = const [
'SELECT', 'UPDATE', 'INSERT', 'DELETE', 'FROM', 'ASC', 'DESC', 'VALUES', 'RETURNING', 'ORDER', 'BY',
];
/// Applies additional attributes to a database column.
class Column {
/// If `true`, a SQL field will be nullable.
final bool nullable;
/// Specifies the length of a `VARCHAR`.
final int length;
/// Explicitly defines a SQL type for this column.
final ColumnType type;
/// Specifies what kind of index this column is, if any.
final IndexType index;
/// The default value of this field.
final defaultValue;
const Column(
{this.nullable: true,
this.length,
this.type,
this.index: IndexType.NONE,
this.defaultValue});
}
class PrimaryKey extends Column {
const PrimaryKey({ColumnType columnType})
: super(
type: columnType ?? ColumnType.SERIAL,
index: IndexType.PRIMARY_KEY);
}
const Column primaryKey = const PrimaryKey();
/// Maps to SQL index types.
enum IndexType {
NONE,
/// Standard index.
INDEX,
/// A primary key.
PRIMARY_KEY,
/// A *unique* index.
UNIQUE
}
/// Maps to SQL data types.
///
/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm
class ColumnType {
/// The name of this data type.
final String name;
const ColumnType(this.name);
static const ColumnType BOOLEAN = const ColumnType('boolean');
static const ColumnType SMALL_SERIAL = const ColumnType('smallserial');
static const ColumnType SERIAL = const ColumnType('serial');
static const ColumnType BIG_SERIAL = const ColumnType('bigserial');
// Numbers
static const ColumnType BIG_INT = const ColumnType('bigint');
static const ColumnType INT = const ColumnType('int');
static const ColumnType SMALL_INT = const ColumnType('smallint');
static const ColumnType TINY_INT = const ColumnType('tinyint');
static const ColumnType BIT = const ColumnType('bit');
static const ColumnType DECIMAL = const ColumnType('decimal');
static const ColumnType NUMERIC = const ColumnType('numeric');
static const ColumnType MONEY = const ColumnType('money');
static const ColumnType SMALL_MONEY = const ColumnType('smallmoney');
static const ColumnType FLOAT = const ColumnType('float');
static const ColumnType REAL = const ColumnType('real');
// Dates and times
static const ColumnType DATE_TIME = const ColumnType('datetime');
static const ColumnType SMALL_DATE_TIME = const ColumnType('smalldatetime');
static const ColumnType DATE = const ColumnType('date');
static const ColumnType TIME = const ColumnType('time');
static const ColumnType TIME_STAMP = const ColumnType('timestamp');
static const ColumnType TIME_STAMP_WITH_TIME_ZONE = const ColumnType('timestamp with time zone');
// Strings
static const ColumnType CHAR = const ColumnType('char');
static const ColumnType VAR_CHAR = const ColumnType('varchar');
static const ColumnType VAR_CHAR_MAX = const ColumnType('varchar(max)');
static const ColumnType TEXT = const ColumnType('text');
// Unicode strings
static const ColumnType NCHAR = const ColumnType('nchar');
static const ColumnType NVAR_CHAR = const ColumnType('nvarchar');
static const ColumnType NVAR_CHAR_MAX = const ColumnType('nvarchar(max)');
static const ColumnType NTEXT = const ColumnType('ntext');
// Binary
static const ColumnType BINARY = const ColumnType('binary');
static const ColumnType VAR_BINARY = const ColumnType('varbinary');
static const ColumnType VAR_BINARY_MAX = const ColumnType('varbinary(max)');
static const ColumnType IMAGE = const ColumnType('image');
// Misc.
static const ColumnType SQL_VARIANT = const ColumnType('sql_variant');
static const ColumnType UNIQUE_IDENTIFIER =
const ColumnType('uniqueidentifier');
static const ColumnType XML = const ColumnType('xml');
static const ColumnType CURSOR = const ColumnType('cursor');
static const ColumnType TABLE = const ColumnType('table');
}

View file

@ -1,69 +0,0 @@
import 'dart:async';
import 'package:pool/pool.dart';
import 'package:postgres/postgres.dart';
/// Connects to a PostgreSQL database, whether synchronously or asynchronously.
typedef FutureOr<PostgreSQLConnection> PostgreSQLConnector();
/// Pools connections to a PostgreSQL database.
class PostgreSQLConnectionPool {
final List<PostgreSQLConnection> _connections = [];
final List<int> _opened = [];
int _index = 0;
Pool _pool;
/// The maximum number of concurrent connections to the database.
///
/// Default: `5`
final int concurrency;
/// An optional timeout for pooled connections to execute.
final Duration timeout;
/// A function that connects this pool to the database, on-demand.
final PostgreSQLConnector connector;
PostgreSQLConnectionPool(this.connector,
{this.concurrency: 5, this.timeout}) {
_pool = new Pool(concurrency, timeout: timeout);
}
Future<PostgreSQLConnection> _connect() async {
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();
_opened.add(connection.hashCode);
}
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].
Future<T> run<T>(FutureOr<T> callback(PostgreSQLConnection connection)) {
return _pool.request().then((resx) {
return _connect().then((connection) {
return new Future<T>.sync(() => callback(connection))
.whenComplete(() async {
if (connection.isClosed) {
_connections
..remove(connection)
..add(await connector());
}
resx.release();
});
});
});
}
}

View file

@ -1,332 +1,114 @@
import 'package:meta/meta.dart';
import 'package:intl/intl.dart';
import 'package:string_scanner/string_scanner.dart';
/// Expects a field to be equal to a given [value].
Predicate<T> equals<T>(T value) =>
new Predicate<T>._(PredicateType.equals, value);
final DateFormat DATE_YMD = new DateFormat('yyyy-MM-dd');
final DateFormat DATE_YMD_HMS = new DateFormat('yyyy-MM-dd HH:mm:ss');
/// Expects at least one of the given [predicates] to be true.
Predicate<T> anyOf<T>(Iterable<Predicate<T>> predicates) =>
new MultiPredicate<T>._(PredicateType.any, predicates);
/// 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;
/// Expects a field to be contained within a set of [values].
Predicate<T> isIn<T>(Iterable<T> values) => new Predicate<T>._(PredicateType.isIn, null, values);
while (!scanner.isDone) {
// Ignore comment starts
if (scanner.scan('--') || scanner.scan('/*'))
continue;
/// Expects a field to be `null`.
Predicate<T> isNull<T>() => equals(null);
// Ignore all single quotes and attempted escape sequences
else if (scanner.scan("'") || scanner.scan('\\'))
continue;
/// Expects a given [predicate] to not be true.
Predicate<T> not<T>(Predicate<T> predicate) =>
new MultiPredicate<T>._(PredicateType.negate, [predicate]);
// Otherwise, add the next char, unless it's a null byte.
else if ((ch = scanner.readChar()) != 0 && ch != null)
buf.writeCharCode(ch);
/// Expects a field to be not be `null`.
Predicate<T> notNull<T>() => not(isNull());
/// Expects a field to be less than a given [value].
Predicate<T> lessThan<T>(T value) =>
new Predicate<T>._(PredicateType.less, value);
/// Expects a field to be less than or equal to a given [value].
Predicate<T> lessThanOrEqual<T>(T value) => lessThan(value) | equals(value);
/// Expects a field to be greater than a given [value].
Predicate<T> greaterThan<T>(T value) =>
new Predicate<T>._(PredicateType.greater, value);
/// Expects a field to be greater than or equal to a given [value].
Predicate<T> greaterThanOrEqual<T>(T value) =>
greaterThan(value) | equals(value);
/// A generic query class.
///
/// Angel services can translate these into driver-specific queries.
/// This allows the Angel ORM to be flexible and support multiple platforms.
class Query {
final Map<String, Predicate> _fields = {};
final Map<String, SortType> _sort = {};
/// Each field in a query is actually a [Predicate], and therefore acts as a contract
/// with the underlying service.
Map<String, Predicate> get fields =>
new Map<String, Predicate>.unmodifiable(_fields);
/// The sorting order applied to this query.
Map<String, SortType> get sorting =>
new Map<String, SortType>.unmodifiable(_sort);
/// Sets the [Predicate] assigned to the given [key].
void operator []=(String key, Predicate value) => _fields[key] = value;
/// Gets the [Predicate] assigned to the given [key].
Predicate operator [](String key) => _fields[key];
/// Sort output by the given [key].
void sortBy(String key, [SortType type = SortType.descending]) =>
_sort[key] = type;
}
return buf.toString();
/// A mechanism used to express an expectation about some object ([target]).
class Predicate<T> {
/// The type of expectation we are declaring.
final PredicateType type;
/// The single argument of this target.
final T target;
final Iterable<T> args;
Predicate._(this.type, this.target, [this.args]);
Predicate<T> operator &(Predicate<T> other) => and(other);
Predicate<T> operator |(Predicate<T> other) => or(other);
Predicate<T> and(Predicate<T> other) {
return new MultiPredicate._(PredicateType.and, [this, other]);
}
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<T extends num>
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;
}
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
return '$_op $_value';
}
operator <(T value) => _change('<', value);
operator >(T value) => _change('>', value);
operator <=(T value) => _change('<=', value);
operator >=(T value) => _change('>=', value);
void lessThan(T value) {
_change('<', value);
}
void lessThanOrEqualTo(T value) {
_change('<=', value);
}
void greaterThan(T value) {
_change('>', value);
}
void greaterThanOrEqualTo(T value) {
_change('>=', value);
}
void equals(T value) {
_change('=', value);
}
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<T> values) {
_raw = 'IN (' + values.join(', ') + ')';
_hasValue = true;
}
@override
void isNotIn(@checked Iterable<T> values) {
_raw = 'NOT IN (' + values.join(', ') + ')';
_hasValue = true;
Predicate<T> or(Predicate<T> other) {
return new MultiPredicate._(PredicateType.or, [this, other]);
}
}
class StringSqlExpressionBuilder implements SqlExpressionBuilder {
bool _hasValue = false;
String _op = '=', _raw, _value;
/// An advanced [Predicate] that performs an operation of multiple other predicates.
class MultiPredicate<T> extends Predicate<T> {
final Iterable<Predicate<T>> targets;
@override
bool get hasValue => _hasValue;
MultiPredicate._(PredicateType type, this.targets) : super._(type, null);
bool _change(String op, String value) {
_raw = null;
_op = op;
_value = value;
return _hasValue = true;
/// Use [targets] instead.
@deprecated
T get target => throw new UnsupportedError(
'IterablePredicate has no `target`. Use `targets` instead.');
}
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
var v = sanitizeExpression(_value);
return "$_op '$v'";
/// The various types of predicate.
enum PredicateType {
equals,
any,
isIn,
negate,
and,
or,
less,
greater,
}
void isEmpty() => equals('');
void equals(String value) {
_change('=', value);
}
void notEquals(String value) {
_change('!=', value);
}
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<String> values) {
_raw = 'IN (' +
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
')';
_hasValue = true;
}
@override
void isNotIn(@checked Iterable<String> values) {
_raw = 'NOT IN (' +
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
')';
_hasValue = true;
}
}
class BooleanSqlExpressionBuilder implements SqlExpressionBuilder {
bool _hasValue = false;
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;
}
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
var v = _value ? 'TRUE' : 'FALSE';
return '$_op $v';
}
void equals(bool value) {
_change('=', value);
}
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<bool> values) {
_raw = 'IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')';
_hasValue = true;
}
@override
void isNotIn(@checked Iterable<bool> values) {
_raw =
'NOT IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')';
_hasValue = true;
}
}
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder {
final NumericSqlExpressionBuilder<int> year =
new NumericSqlExpressionBuilder<int>(),
month = new NumericSqlExpressionBuilder<int>(),
day = new NumericSqlExpressionBuilder<int>(),
hour = new NumericSqlExpressionBuilder<int>(),
minute = new NumericSqlExpressionBuilder<int>(),
second = new NumericSqlExpressionBuilder<int>();
final String columnName;
String _raw;
DateTimeSqlExpressionBuilder(this.columnName);
@override
bool get hasValue =>
_raw?.isNotEmpty == true ||
year.hasValue ||
month.hasValue ||
day.hasValue ||
hour.hasValue ||
minute.hasValue ||
second.hasValue;
bool _change(String _op, DateTime dt, bool time) {
var dateString = time ? DATE_YMD_HMS.format(dt) : DATE_YMD.format(dt);
_raw = '$columnName $_op \'$dateString\'';
return true;
}
operator <(DateTime value) => _change('<', value, true);
operator <=(DateTime value) => _change('<=', value, true);
operator >(DateTime value) => _change('>', value, true);
operator >=(DateTime value) => _change('>=', value, true);
void equals(DateTime value, {bool includeTime: true}) {
_change('=', value, includeTime != false);
}
void lessThan(DateTime value, {bool includeTime: true}) {
_change('<', value, includeTime != false);
}
void lessThanOrEqualTo(DateTime value, {bool includeTime: true}) {
_change('<=', value, includeTime != false);
}
void greaterThan(DateTime value, {bool includeTime: true}) {
_change('>', value, includeTime != false);
}
void greaterThanOrEqualTo(DateTime value, {bool includeTime: true}) {
_change('>=', value, includeTime != false);
}
@override
void isIn(@checked Iterable<DateTime> values) {
_raw = '$columnName IN (' +
values.map(DATE_YMD_HMS.format).map((s) => '$s').join(', ') +
')';
}
@override
void isNotIn(@checked Iterable<DateTime> 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;
List<String> parts = [];
if (year.hasValue) parts.add('YEAR($columnName) ${year.compile()}');
if (month.hasValue) parts.add('MONTH($columnName) ${month.compile()}');
if (day.hasValue) parts.add('DAY($columnName) ${day.compile()}');
if (hour.hasValue) parts.add('HOUR($columnName) ${hour.compile()}');
if (minute.hasValue) parts.add('MINUTE($columnName) ${minute.compile()}');
if (second.hasValue) parts.add('SECOND($columnName) ${second.compile()}');
return parts.isEmpty ? null : parts.join(' AND ');
}
}
/// The various modes of sorting.
enum SortType { ascending, descending }

View file

@ -1,13 +1,10 @@
name: angel_orm
version: 1.0.0-alpha+12
version: 1.0.0-alpha+11
description: Runtime support for Angel's ORM.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/orm
environment:
sdk: ">=1.19.0"
sdk: '>=2.0.0-dev.1.2 <2.0.0'
dependencies:
intl: ">=0.0.0 <1.0.0"
angel_model: ^1.0.0
meta: ^1.0.0
pool: ^1.0.0
postgres: ^0.9.5
string_scanner: ^1.0.0

View file

@ -9,7 +9,7 @@ part 'order.g.dart';
@orm
@serializable
class _Order extends Model {
@CanJoin(Customer, 'id')
@Join(Customer, 'id')
int customerId;
int employeeId;
DateTime orderDate;