use substitutionValues for inserts and strings
This commit is contained in:
parent
b2e6c14386
commit
047dc76408
13 changed files with 180 additions and 103 deletions
|
@ -39,7 +39,8 @@ Your model, courtesy of `package:angel_serialize`:
|
||||||
```dart
|
```dart
|
||||||
library angel_orm.test.models.car;
|
library angel_orm.test.models.car;
|
||||||
|
|
||||||
import 'package:angel_framework/common.dart';
|
import 'package:angel_migration/angel_migration.dart';
|
||||||
|
import 'package:angel_model/angel_model.dart';
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
import 'package:angel_orm/angel_orm.dart';
|
||||||
import 'package:angel_serialize/angel_serialize.dart';
|
import 'package:angel_serialize/angel_serialize.dart';
|
||||||
part 'car.g.dart';
|
part 'car.g.dart';
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
# 2.0.0-dev.15
|
||||||
|
* Remove `Column.defaultValue`.
|
||||||
|
* Deprecate `toSql` and `sanitizeExpression`.
|
||||||
|
* Refactor builders so that strings are passed through
|
||||||
|
|
||||||
# 2.0.0-dev.14
|
# 2.0.0-dev.14
|
||||||
* Remove obsolete `@belongsToMany`.
|
* Remove obsolete `@belongsToMany`.
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,12 @@ class _FakeExecutor extends QueryExecutor {
|
||||||
const _FakeExecutor();
|
const _FakeExecutor();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<List>> query(String query, [returningFields]) async {
|
Future<List<List>> query(
|
||||||
|
String query, Map<String, dynamic> substitutionValues,
|
||||||
|
[returningFields]) async {
|
||||||
var now = new DateTime.now();
|
var now = new DateTime.now();
|
||||||
print('_FakeExecutor received query: $query');
|
print(
|
||||||
|
'_FakeExecutor received query: $query and values: $substitutionValues');
|
||||||
return [
|
return [
|
||||||
[1, 'Rich', 'Person', 100000.0, now, now]
|
[1, 'Rich', 'Person', 100000.0, now, now]
|
||||||
];
|
];
|
||||||
|
@ -50,8 +53,14 @@ class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
|
||||||
@override
|
@override
|
||||||
final QueryValues values = new MapQueryValues();
|
final QueryValues values = new MapQueryValues();
|
||||||
|
|
||||||
|
EmployeeQueryWhere _where;
|
||||||
|
|
||||||
|
EmployeeQuery() {
|
||||||
|
_where = new EmployeeQueryWhere(this);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final EmployeeQueryWhere where = new EmployeeQueryWhere();
|
EmployeeQueryWhere get where => _where;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tableName => 'employees';
|
String get tableName => 'employees';
|
||||||
|
@ -60,6 +69,9 @@ class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
|
||||||
List<String> get fields =>
|
List<String> get fields =>
|
||||||
['id', 'first_name', 'last_name', 'salary', 'created_at', 'updated_at'];
|
['id', 'first_name', 'last_name', 'salary', 'created_at', 'updated_at'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
EmployeeQueryWhere newWhereClause() => new EmployeeQueryWhere(this);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Employee deserialize(List row) {
|
Employee deserialize(List row) {
|
||||||
return new Employee(
|
return new Employee(
|
||||||
|
@ -73,26 +85,28 @@ class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmployeeQueryWhere extends QueryWhere {
|
class EmployeeQueryWhere extends QueryWhere {
|
||||||
|
EmployeeQueryWhere(EmployeeQuery query)
|
||||||
|
: id = new NumericSqlExpressionBuilder(query, 'id'),
|
||||||
|
firstName = new StringSqlExpressionBuilder(query, 'first_name'),
|
||||||
|
lastName = new StringSqlExpressionBuilder(query, 'last_name'),
|
||||||
|
salary = new NumericSqlExpressionBuilder(query, 'salary'),
|
||||||
|
createdAt = new DateTimeSqlExpressionBuilder(query, 'created_at'),
|
||||||
|
updatedAt = new DateTimeSqlExpressionBuilder(query, 'updated_at');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Iterable<SqlExpressionBuilder> get expressionBuilders {
|
Iterable<SqlExpressionBuilder> get expressionBuilders {
|
||||||
return [id, firstName, lastName, salary, createdAt, updatedAt];
|
return [id, firstName, lastName, salary, createdAt, updatedAt];
|
||||||
}
|
}
|
||||||
|
|
||||||
final NumericSqlExpressionBuilder<int> id =
|
final NumericSqlExpressionBuilder<int> id;
|
||||||
new NumericSqlExpressionBuilder<int>('id');
|
|
||||||
|
|
||||||
final StringSqlExpressionBuilder firstName =
|
final StringSqlExpressionBuilder firstName;
|
||||||
new StringSqlExpressionBuilder('first_name');
|
|
||||||
|
|
||||||
final StringSqlExpressionBuilder lastName =
|
final StringSqlExpressionBuilder lastName;
|
||||||
new StringSqlExpressionBuilder('last_name');
|
|
||||||
|
|
||||||
final NumericSqlExpressionBuilder<double> salary =
|
final NumericSqlExpressionBuilder<double> salary;
|
||||||
new NumericSqlExpressionBuilder<double>('salary');
|
|
||||||
|
|
||||||
final DateTimeSqlExpressionBuilder createdAt =
|
final DateTimeSqlExpressionBuilder createdAt;
|
||||||
new DateTimeSqlExpressionBuilder('created_at');
|
|
||||||
|
|
||||||
final DateTimeSqlExpressionBuilder updatedAt =
|
final DateTimeSqlExpressionBuilder updatedAt;
|
||||||
new DateTimeSqlExpressionBuilder('updated_at');
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ const Orm orm = const Orm();
|
||||||
|
|
||||||
class Orm {
|
class Orm {
|
||||||
/// The name of the table to query.
|
/// The name of the table to query.
|
||||||
///
|
///
|
||||||
/// Inferred if not present.
|
/// Inferred if not present.
|
||||||
final String tableName;
|
final String tableName;
|
||||||
|
|
||||||
/// Whether to generate migrations for this model.
|
/// Whether to generate migrations for this model.
|
||||||
///
|
///
|
||||||
/// Defaults to [:true:].
|
/// Defaults to [:true:].
|
||||||
final bool generateMigrations;
|
final bool generateMigrations;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@ import 'query.dart';
|
||||||
final DateFormat dateYmd = new DateFormat('yyyy-MM-dd');
|
final DateFormat dateYmd = new DateFormat('yyyy-MM-dd');
|
||||||
final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss');
|
final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
|
|
||||||
/// Cleans an input SQL expression of common SQL injection points.
|
/// The ORM prefers using substitution values, which allow for prepared queries,
|
||||||
|
/// and prevent SQL injection attacks.
|
||||||
|
@deprecated
|
||||||
String sanitizeExpression(String unsafe) {
|
String sanitizeExpression(String unsafe) {
|
||||||
var buf = new StringBuffer();
|
var buf = new StringBuffer();
|
||||||
var scanner = new StringScanner(unsafe);
|
var scanner = new StringScanner(unsafe);
|
||||||
|
@ -30,7 +32,13 @@ String sanitizeExpression(String unsafe) {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SqlExpressionBuilder<T> {
|
abstract class SqlExpressionBuilder<T> {
|
||||||
String get columnName;
|
final Query query;
|
||||||
|
final String columnName;
|
||||||
|
String _substitution;
|
||||||
|
|
||||||
|
SqlExpressionBuilder(this.query, this.columnName);
|
||||||
|
|
||||||
|
String get substitution => _substitution ??= query.reserveName(columnName);
|
||||||
|
|
||||||
bool get hasValue;
|
bool get hasValue;
|
||||||
|
|
||||||
|
@ -46,14 +54,14 @@ abstract class SqlExpressionBuilder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NumericSqlExpressionBuilder<T extends num>
|
class NumericSqlExpressionBuilder<T extends num>
|
||||||
implements SqlExpressionBuilder<T> {
|
extends SqlExpressionBuilder<T> {
|
||||||
final String columnName;
|
|
||||||
bool _hasValue = false;
|
bool _hasValue = false;
|
||||||
String _op = '=';
|
String _op = '=';
|
||||||
String _raw;
|
String _raw;
|
||||||
T _value;
|
T _value;
|
||||||
|
|
||||||
NumericSqlExpressionBuilder(this.columnName);
|
NumericSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get hasValue => _hasValue;
|
bool get hasValue => _hasValue;
|
||||||
|
@ -129,20 +137,25 @@ class NumericSqlExpressionBuilder<T extends num>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
class StringSqlExpressionBuilder extends SqlExpressionBuilder<String> {
|
||||||
final String columnName;
|
|
||||||
bool _hasValue = false;
|
bool _hasValue = false;
|
||||||
String _op = '=', _raw, _value;
|
String _op = '=', _raw, _value;
|
||||||
|
|
||||||
StringSqlExpressionBuilder(this.columnName);
|
StringSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get hasValue => _hasValue;
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
|
String get lowerName => '${substitution}_lower';
|
||||||
|
|
||||||
|
String get upperName => '${substitution}_upper';
|
||||||
|
|
||||||
bool _change(String op, String value) {
|
bool _change(String op, String value) {
|
||||||
_raw = null;
|
_raw = null;
|
||||||
_op = op;
|
_op = op;
|
||||||
_value = value;
|
_value = value;
|
||||||
|
query.substitutionValues[substitution] = _value;
|
||||||
return _hasValue = true;
|
return _hasValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +163,7 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
||||||
String compile() {
|
String compile() {
|
||||||
if (_raw != null) return _raw;
|
if (_raw != null) return _raw;
|
||||||
if (_value == null) return null;
|
if (_value == null) return null;
|
||||||
var v = toSql(_value);
|
return "$_op @$substitution";
|
||||||
return "$_op $v";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void isEmpty() => equals('');
|
void isEmpty() => equals('');
|
||||||
|
@ -164,48 +176,67 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
||||||
_change('!=', value);
|
_change('!=', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void like(String value) {
|
/// Builds a `LIKE` predicate.
|
||||||
_change('LIKE', value);
|
///
|
||||||
|
/// To prevent injections, the [pattern] is called with a name that
|
||||||
|
/// will be escaped by the underlying [QueryExecutor].
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// carNameBuilder.like((name) => 'Mazda %$name%');
|
||||||
|
/// ```
|
||||||
|
void like(String Function(String) pattern) {
|
||||||
|
_raw = 'LIKE \'' + pattern('@$substitution') + '\'';
|
||||||
|
query.substitutionValues[substitution] = _value;
|
||||||
|
_hasValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void isBetween(String lower, String upper) {
|
void isBetween(String lower, String upper) {
|
||||||
var l = sanitizeExpression(lower), u = sanitizeExpression(upper);
|
query.substitutionValues[lowerName] = lower;
|
||||||
_raw = "BETWEEN '$l' AND '$u'";
|
query.substitutionValues[upperName] = upper;
|
||||||
|
_raw = "BETWEEN @$lowerName AND @$upperName";
|
||||||
_hasValue = true;
|
_hasValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void isNotBetween(String lower, String upper) {
|
void isNotBetween(String lower, String upper) {
|
||||||
var l = sanitizeExpression(lower), u = sanitizeExpression(upper);
|
query.substitutionValues[lowerName] = lower;
|
||||||
_raw = "NOT BETWEEN '$l' AND '$u'";
|
query.substitutionValues[upperName] = upper;
|
||||||
|
_raw = "NOT BETWEEN @$lowerName AND @$upperName";
|
||||||
_hasValue = true;
|
_hasValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _in(Iterable<String> values) {
|
||||||
|
return 'IN (' +
|
||||||
|
values.map((v) {
|
||||||
|
var name = query.reserveName('${columnName}_in_value');
|
||||||
|
query.substitutionValues[name] = v;
|
||||||
|
return '@$name';
|
||||||
|
}).join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void isIn(Iterable<String> values) {
|
void isIn(Iterable<String> values) {
|
||||||
_raw = 'IN (' +
|
_raw = _in(values);
|
||||||
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
|
|
||||||
')';
|
|
||||||
_hasValue = true;
|
_hasValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void isNotIn(Iterable<String> values) {
|
void isNotIn(Iterable<String> values) {
|
||||||
_raw = 'NOT IN (' +
|
_raw = 'NOT ' + _in(values);
|
||||||
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
|
|
||||||
')';
|
|
||||||
_hasValue = true;
|
_hasValue = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
|
class BooleanSqlExpressionBuilder extends SqlExpressionBuilder<bool> {
|
||||||
final String columnName;
|
|
||||||
bool _hasValue = false;
|
bool _hasValue = false;
|
||||||
String _op = '=', _raw;
|
String _op = '=', _raw;
|
||||||
bool _value;
|
bool _value;
|
||||||
|
|
||||||
BooleanSqlExpressionBuilder(this.columnName);
|
BooleanSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get hasValue => _hasValue;
|
bool get hasValue => _hasValue;
|
||||||
|
@ -258,28 +289,36 @@ class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> {
|
class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder<DateTime> {
|
||||||
final NumericSqlExpressionBuilder<int> year =
|
NumericSqlExpressionBuilder<int> _year, _month, _day, _hour, _minute, _second;
|
||||||
new NumericSqlExpressionBuilder<int>('year'),
|
|
||||||
month = new NumericSqlExpressionBuilder<int>('month'),
|
|
||||||
day = new NumericSqlExpressionBuilder<int>('day'),
|
|
||||||
hour = new NumericSqlExpressionBuilder<int>('hour'),
|
|
||||||
minute = new NumericSqlExpressionBuilder<int>('minute'),
|
|
||||||
second = new NumericSqlExpressionBuilder<int>('second');
|
|
||||||
final String columnName;
|
|
||||||
String _raw;
|
String _raw;
|
||||||
|
|
||||||
DateTimeSqlExpressionBuilder(this.columnName);
|
DateTimeSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder<int> get year =>
|
||||||
|
_year ??= new NumericSqlExpressionBuilder(query, 'year');
|
||||||
|
NumericSqlExpressionBuilder<int> get month =>
|
||||||
|
_month ??= new NumericSqlExpressionBuilder(query, 'month');
|
||||||
|
NumericSqlExpressionBuilder<int> get day =>
|
||||||
|
_day ??= new NumericSqlExpressionBuilder(query, 'day');
|
||||||
|
NumericSqlExpressionBuilder<int> get hour =>
|
||||||
|
_hour ??= new NumericSqlExpressionBuilder(query, 'hour');
|
||||||
|
NumericSqlExpressionBuilder<int> get minute =>
|
||||||
|
_minute ??= new NumericSqlExpressionBuilder(query, 'minute');
|
||||||
|
NumericSqlExpressionBuilder<int> get second =>
|
||||||
|
_second ??= new NumericSqlExpressionBuilder(query, 'second');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get hasValue =>
|
bool get hasValue =>
|
||||||
_raw?.isNotEmpty == true ||
|
_raw?.isNotEmpty == true ||
|
||||||
year.hasValue ||
|
_year?.hasValue == true ||
|
||||||
month.hasValue ||
|
_month?.hasValue == true ||
|
||||||
day.hasValue ||
|
_day?.hasValue == true ||
|
||||||
hour.hasValue ||
|
_hour?.hasValue == true ||
|
||||||
minute.hasValue ||
|
_minute?.hasValue == true ||
|
||||||
second.hasValue;
|
_second?.hasValue == true;
|
||||||
|
|
||||||
bool _change(String _op, DateTime dt, bool time) {
|
bool _change(String _op, DateTime dt, bool time) {
|
||||||
var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt);
|
var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt);
|
||||||
|
@ -345,12 +384,17 @@ class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> {
|
||||||
String compile() {
|
String compile() {
|
||||||
if (_raw?.isNotEmpty == true) return _raw;
|
if (_raw?.isNotEmpty == true) return _raw;
|
||||||
List<String> parts = [];
|
List<String> parts = [];
|
||||||
if (year.hasValue) parts.add('YEAR($columnName) ${year.compile()}');
|
if (year?.hasValue == true)
|
||||||
if (month.hasValue) parts.add('MONTH($columnName) ${month.compile()}');
|
parts.add('YEAR($columnName) ${year.compile()}');
|
||||||
if (day.hasValue) parts.add('DAY($columnName) ${day.compile()}');
|
if (month?.hasValue == true)
|
||||||
if (hour.hasValue) parts.add('HOUR($columnName) ${hour.compile()}');
|
parts.add('MONTH($columnName) ${month.compile()}');
|
||||||
if (minute.hasValue) parts.add('MINUTE($columnName) ${minute.compile()}');
|
if (day?.hasValue == true) parts.add('DAY($columnName) ${day.compile()}');
|
||||||
if (second.hasValue) parts.add('SECOND($columnName) ${second.compile()}');
|
if (hour?.hasValue == true)
|
||||||
|
parts.add('HOUR($columnName) ${hour.compile()}');
|
||||||
|
if (minute?.hasValue == true)
|
||||||
|
parts.add('MINUTE($columnName) ${minute.compile()}');
|
||||||
|
if (second?.hasValue == true)
|
||||||
|
parts.add('SECOND($columnName) ${second.compile()}');
|
||||||
|
|
||||||
return parts.isEmpty ? null : parts.join(' AND ');
|
return parts.isEmpty ? null : parts.join(' AND ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,15 +26,11 @@ class Column {
|
||||||
/// Specifies what kind of index this column is, if any.
|
/// Specifies what kind of index this column is, if any.
|
||||||
final IndexType indexType;
|
final IndexType indexType;
|
||||||
|
|
||||||
/// The default value of this field.
|
|
||||||
final defaultValue;
|
|
||||||
|
|
||||||
const Column(
|
const Column(
|
||||||
{this.isNullable: true,
|
{this.isNullable: true,
|
||||||
this.length,
|
this.length,
|
||||||
this.type,
|
this.type,
|
||||||
this.indexType: IndexType.none,
|
this.indexType: IndexType.none});
|
||||||
this.defaultValue});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PrimaryKey extends Column {
|
class PrimaryKey extends Column {
|
||||||
|
|
|
@ -7,6 +7,8 @@ bool isAscii(int ch) => ch >= $nul && ch <= $del;
|
||||||
|
|
||||||
/// A base class for objects that compile to SQL queries, typically within an ORM.
|
/// A base class for objects that compile to SQL queries, typically within an ORM.
|
||||||
abstract class QueryBase<T> {
|
abstract class QueryBase<T> {
|
||||||
|
final Map<String, dynamic> substitutionValues = {};
|
||||||
|
|
||||||
/// The list of fields returned by this query.
|
/// The list of fields returned by this query.
|
||||||
///
|
///
|
||||||
/// If it's `null`, then this query will perform a `SELECT *`.
|
/// If it's `null`, then this query will perform a `SELECT *`.
|
||||||
|
@ -22,7 +24,9 @@ abstract class QueryBase<T> {
|
||||||
|
|
||||||
Future<List<T>> get(QueryExecutor executor) async {
|
Future<List<T>> get(QueryExecutor executor) async {
|
||||||
var sql = compile();
|
var sql = compile();
|
||||||
return executor.query(sql).then((it) => it.map(deserialize).toList());
|
return executor
|
||||||
|
.query(sql, substitutionValues)
|
||||||
|
.then((it) => it.map(deserialize).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> getOne(QueryExecutor executor) {
|
Future<T> getOne(QueryExecutor executor) {
|
||||||
|
@ -47,6 +51,9 @@ class OrderBy {
|
||||||
String compile() => descending ? '$key DESC' : '$key ASC';
|
String compile() => descending ? '$key DESC' : '$key ASC';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ORM prefers using substitution values, which allow for prepared queries,
|
||||||
|
/// and prevent SQL injection attacks.
|
||||||
|
@deprecated
|
||||||
String toSql(Object obj, {bool withQuotes: true}) {
|
String toSql(Object obj, {bool withQuotes: true}) {
|
||||||
if (obj is DateTime) {
|
if (obj is DateTime) {
|
||||||
return withQuotes ? "'${dateYmdHms.format(obj)}'" : dateYmdHms.format(obj);
|
return withQuotes ? "'${dateYmdHms.format(obj)}'" : dateYmdHms.format(obj);
|
||||||
|
@ -96,7 +103,9 @@ String toSql(Object obj, {bool withQuotes: true}) {
|
||||||
/// A SQL `SELECT` query builder.
|
/// A SQL `SELECT` query builder.
|
||||||
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
final List<JoinBuilder> _joins = [];
|
final List<JoinBuilder> _joins = [];
|
||||||
|
final Map<String, int> _names = {};
|
||||||
final List<OrderBy> _orderBy = [];
|
final List<OrderBy> _orderBy = [];
|
||||||
|
|
||||||
String _crossJoin, _groupBy;
|
String _crossJoin, _groupBy;
|
||||||
int _limit, _offset;
|
int _limit, _offset;
|
||||||
|
|
||||||
|
@ -113,8 +122,17 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
/// This is usually a generated class.
|
/// This is usually a generated class.
|
||||||
QueryValues get values;
|
QueryValues get values;
|
||||||
|
|
||||||
|
/// Preprends the [tableName] to the [String], [s].
|
||||||
String adornWithTableName(String s) => '$tableName.$s';
|
String adornWithTableName(String s) => '$tableName.$s';
|
||||||
|
|
||||||
|
/// Returns a unique version of [name], which will not produce a collision within
|
||||||
|
/// the context of this [query].
|
||||||
|
String reserveName(String name) {
|
||||||
|
var n = _names[name] ??= 0;
|
||||||
|
_names[name]++;
|
||||||
|
return n == 0 ? name : '${name}$n';
|
||||||
|
}
|
||||||
|
|
||||||
/// Makes a new [Where] clause.
|
/// Makes a new [Where] clause.
|
||||||
Where newWhereClause() {
|
Where newWhereClause() {
|
||||||
throw new UnsupportedError(
|
throw new UnsupportedError(
|
||||||
|
@ -258,14 +276,15 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
|
|
||||||
if (_joins.isEmpty) {
|
if (_joins.isEmpty) {
|
||||||
return executor
|
return executor
|
||||||
.query(sql, fields.map(adornWithTableName).toList())
|
.query(
|
||||||
|
sql, substitutionValues, fields.map(adornWithTableName).toList())
|
||||||
.then((it) => it.map(deserialize).toList());
|
.then((it) => it.map(deserialize).toList());
|
||||||
} else {
|
} else {
|
||||||
return executor.transaction(() async {
|
return executor.transaction(() async {
|
||||||
// TODO: Can this be done with just *one* query?
|
// TODO: Can this be done with just *one* query?
|
||||||
var existing = await get(executor);
|
var existing = await get(executor);
|
||||||
//var sql = compile(preamble: 'SELECT $tableName.id', withFields: false);
|
//var sql = compile(preamble: 'SELECT $tableName.id', withFields: false);
|
||||||
return executor.query(sql).then((_) => existing);
|
return executor.query(sql, substitutionValues).then((_) => existing);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,20 +294,21 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> insert(QueryExecutor executor) {
|
Future<T> insert(QueryExecutor executor) {
|
||||||
var sql = values.compileInsert(tableName);
|
var sql = values.compileInsert(this, tableName);
|
||||||
|
|
||||||
if (sql == null) {
|
if (sql == null) {
|
||||||
throw new StateError('No values have been specified for update.');
|
throw new StateError('No values have been specified for update.');
|
||||||
} else {
|
} else {
|
||||||
return executor
|
return executor
|
||||||
.query(sql, fields.map(adornWithTableName).toList())
|
.query(
|
||||||
|
sql, substitutionValues, fields.map(adornWithTableName).toList())
|
||||||
.then((it) => it.isEmpty ? null : deserialize(it.first));
|
.then((it) => it.isEmpty ? null : deserialize(it.first));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<T>> update(QueryExecutor executor) async {
|
Future<List<T>> update(QueryExecutor executor) async {
|
||||||
var sql = new StringBuffer('UPDATE $tableName ');
|
var sql = new StringBuffer('UPDATE $tableName ');
|
||||||
var valuesClause = values.compileForUpdate();
|
var valuesClause = values.compileForUpdate(this);
|
||||||
|
|
||||||
if (valuesClause == null) {
|
if (valuesClause == null) {
|
||||||
throw new StateError('No values have been specified for update.');
|
throw new StateError('No values have been specified for update.');
|
||||||
|
@ -300,12 +320,14 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
|
|
||||||
if (_joins.isEmpty) {
|
if (_joins.isEmpty) {
|
||||||
return executor
|
return executor
|
||||||
.query(sql.toString(), fields.map(adornWithTableName).toList())
|
.query(sql.toString(), substitutionValues,
|
||||||
|
fields.map(adornWithTableName).toList())
|
||||||
.then((it) => it.map(deserialize).toList());
|
.then((it) => it.map(deserialize).toList());
|
||||||
} else {
|
} else {
|
||||||
// TODO: Can this be done with just *one* query?
|
// TODO: Can this be done with just *one* query?
|
||||||
return executor
|
return executor
|
||||||
.query(sql.toString(), fields.map(adornWithTableName).toList())
|
.query(sql.toString(), substitutionValues,
|
||||||
|
fields.map(adornWithTableName).toList())
|
||||||
.then((it) => get(executor));
|
.then((it) => get(executor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,7 +341,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
abstract class QueryValues {
|
abstract class QueryValues {
|
||||||
Map<String, dynamic> toMap();
|
Map<String, dynamic> toMap();
|
||||||
|
|
||||||
String compileInsert(String tableName) {
|
String compileInsert(Query query, String tableName) {
|
||||||
var data = toMap();
|
var data = toMap();
|
||||||
if (data.isEmpty) return null;
|
if (data.isEmpty) return null;
|
||||||
|
|
||||||
|
@ -329,14 +351,17 @@ abstract class QueryValues {
|
||||||
|
|
||||||
for (var entry in data.entries) {
|
for (var entry in data.entries) {
|
||||||
if (i++ > 0) b.write(', ');
|
if (i++ > 0) b.write(', ');
|
||||||
b.write(toSql(entry.value));
|
|
||||||
|
var name = query.reserveName(entry.key);
|
||||||
|
query.substitutionValues[name] = entry.value;
|
||||||
|
b.write('@$name');
|
||||||
}
|
}
|
||||||
|
|
||||||
b.write(')');
|
b.write(')');
|
||||||
return b.toString();
|
return b.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
String compileForUpdate() {
|
String compileForUpdate(Query query) {
|
||||||
var data = toMap();
|
var data = toMap();
|
||||||
if (data.isEmpty) return null;
|
if (data.isEmpty) return null;
|
||||||
var b = new StringBuffer('SET');
|
var b = new StringBuffer('SET');
|
||||||
|
@ -347,7 +372,10 @@ abstract class QueryValues {
|
||||||
b.write(' ');
|
b.write(' ');
|
||||||
b.write(entry.key);
|
b.write(entry.key);
|
||||||
b.write('=');
|
b.write('=');
|
||||||
b.write(toSql(entry.value));
|
|
||||||
|
var name = query.reserveName(entry.key);
|
||||||
|
query.substitutionValues[name] = entry.value;
|
||||||
|
b.write('@$name');
|
||||||
}
|
}
|
||||||
return b.toString();
|
return b.toString();
|
||||||
}
|
}
|
||||||
|
@ -504,7 +532,9 @@ class JoinOn {
|
||||||
abstract class QueryExecutor {
|
abstract class QueryExecutor {
|
||||||
const QueryExecutor();
|
const QueryExecutor();
|
||||||
|
|
||||||
Future<List<List>> query(String query, [List<String> returningFields]);
|
Future<List<List>> query(
|
||||||
|
String query, Map<String, dynamic> substitutionValues,
|
||||||
|
[List<String> returningFields]);
|
||||||
|
|
||||||
Future<T> transaction<T>(FutureOr<T> f());
|
Future<T> transaction<T>(FutureOr<T> f());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: angel_orm
|
name: angel_orm
|
||||||
version: 2.0.0-dev.14
|
version: 2.0.0-dev.15
|
||||||
description: Runtime support for Angel's ORM. Includes base classes for queries.
|
description: Runtime support for Angel's ORM. Includes base classes for queries.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/orm
|
homepage: https://github.com/angel-dart/orm
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('simple', () {
|
|
||||||
expect(toSql('ABC _!'), "'ABC _!'");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ignores null byte', () {
|
|
||||||
expect(toSql('a\x00bc'), "'abc'");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('unicode', () {
|
|
||||||
expect(toSql('東'), r"'\u6771'");
|
|
||||||
expect(toSql('𐐀'), r"'\U00010400'");
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -67,7 +67,8 @@ Future<OrmBuildContext> buildOrmContext(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (column == null && field.name == 'id' && autoIdAndDateFields == true) {
|
if (column == null && field.name == 'id' && autoIdAndDateFields == true) {
|
||||||
// TODO: This is only for PostgreSQL!!!
|
// This is only for PostgreSQL, so implementations without a `serial` type
|
||||||
|
// must handle it accordingly, of course.
|
||||||
column = const Column(type: ColumnType.serial);
|
column = const Column(type: ColumnType.serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
library angel_orm_generator.test.models.customer;
|
library angel_orm_generator.test.models.customer;
|
||||||
|
|
||||||
|
import 'package:angel_migration/angel_migration.dart';
|
||||||
import 'package:angel_model/angel_model.dart';
|
import 'package:angel_model/angel_model.dart';
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
import 'package:angel_orm/angel_orm.dart';
|
||||||
import 'package:angel_serialize/angel_serialize.dart';
|
import 'package:angel_serialize/angel_serialize.dart';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
library angel_orm_generator.test.models.foot;
|
library angel_orm_generator.test.models.foot;
|
||||||
|
|
||||||
|
import 'package:angel_migration/angel_migration.dart';
|
||||||
import 'package:angel_model/angel_model.dart';
|
import 'package:angel_model/angel_model.dart';
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
import 'package:angel_orm/angel_orm.dart';
|
||||||
import 'package:angel_serialize/angel_serialize.dart';
|
import 'package:angel_serialize/angel_serialize.dart';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
library angel_orm_generator.test.models.order;
|
library angel_orm_generator.test.models.order;
|
||||||
|
|
||||||
|
import 'package:angel_migration/angel_migration.dart';
|
||||||
import 'package:angel_model/angel_model.dart';
|
import 'package:angel_model/angel_model.dart';
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
import 'package:angel_orm/angel_orm.dart';
|
||||||
import 'package:angel_serialize/angel_serialize.dart';
|
import 'package:angel_serialize/angel_serialize.dart';
|
||||||
|
|
Loading…
Reference in a new issue