Split up query.dart into separate files

This commit is contained in:
Tobe O 2019-08-17 17:42:55 -04:00
parent 6bad589fde
commit a0d3029ac0
13 changed files with 378 additions and 350 deletions

View file

@ -1,5 +1,15 @@
export 'src/annotations.dart';
export 'src/builder.dart';
export 'src/join_builder.dart';
export 'src/join_on.dart';
export 'src/map_query_values.dart';
export 'src/migration.dart';
export 'src/relations.dart';
export 'src/order_by.dart';
export 'src/query_base.dart';
export 'src/query_executor.dart';
export 'src/query_values.dart';
export 'src/query_where.dart';
export 'src/query.dart';
export 'src/relations.dart';
export 'src/union.dart';
export 'src/util.dart';

View file

@ -1,9 +1,9 @@
import 'dart:convert';
import 'package:charcode/ascii.dart';
import 'package:intl/intl.dart' show DateFormat;
import 'package:string_scanner/string_scanner.dart';
import 'query.dart';
import 'util.dart';
final DateFormat dateYmd = DateFormat('yyyy-MM-dd');
final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss');

View file

@ -0,0 +1,58 @@
import 'annotations.dart';
import 'query.dart';
/// Builds a SQL `JOIN` query.
class JoinBuilder {
final JoinType type;
final Query from;
final String to, key, value, op, alias;
final List<String> additionalFields;
JoinBuilder(this.type, this.from, this.to, this.key, this.value,
{this.op = '=', this.alias, this.additionalFields = const []}) {
assert(to != null,
'computation of this join threw an error, and returned null.');
}
String get fieldName {
var right = '$to.$value';
if (alias != null) right = '$alias.$value';
return right;
}
String nameFor(String name) {
var right = '$to.$name';
if (alias != null) right = '$alias.$name';
return right;
}
String compile(Set<String> trampoline) {
if (to == null) return null;
var b = StringBuffer();
var left = '${from.tableName}.$key';
var right = fieldName;
switch (type) {
case JoinType.inner:
b.write(' INNER JOIN');
break;
case JoinType.left:
b.write(' LEFT JOIN');
break;
case JoinType.right:
b.write(' RIGHT JOIN');
break;
case JoinType.full:
b.write(' FULL OUTER JOIN');
break;
case JoinType.self:
b.write(' SELF JOIN');
break;
}
b.write(' $to');
if (alias != null) b.write(' $alias');
b.write(' ON $left$op$right');
return b.toString();
}
}

View file

@ -0,0 +1,8 @@
import 'builder.dart';
class JoinOn {
final SqlExpressionBuilder key;
final SqlExpressionBuilder value;
JoinOn(this.key, this.value);
}

View file

View file

@ -0,0 +1,8 @@
class OrderBy {
final String key;
final bool descending;
const OrderBy(this.key, {this.descending = false});
String compile() => descending ? '$key DESC' : '$key ASC';
}

View file

@ -1,115 +1,11 @@
import 'dart:async';
import 'package:charcode/ascii.dart';
import 'annotations.dart';
import 'builder.dart';
bool isAscii(int ch) => ch >= $nul && ch <= $del;
/// A base class for objects that compile to SQL queries, typically within an ORM.
abstract class QueryBase<T> {
/// Casts to perform when querying the database.
Map<String, String> get casts => {};
/// Values to insert into a prepared statement.
final Map<String, dynamic> substitutionValues = {};
/// The table against which to execute this query.
String get tableName;
/// The list of fields returned by this query.
///
/// If it's `null`, then this query will perform a `SELECT *`.
List<String> get fields;
/// A String of all [fields], joined by a comma (`,`).
String get fieldSet => fields.map((k) {
var cast = casts[k];
return cast == null ? k : 'CAST ($k AS $cast)';
}).join(', ');
String compile(Set<String> trampoline,
{bool includeTableName = false, String preamble, bool withFields = true});
T deserialize(List row);
Future<List<T>> get(QueryExecutor executor) async {
var sql = compile(Set());
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.map(deserialize).toList());
}
Future<T> getOne(QueryExecutor executor) {
return get(executor).then((it) => it.isEmpty ? null : it.first);
}
Union<T> union(QueryBase<T> other) {
return Union(this, other);
}
Union<T> unionAll(QueryBase<T> other) {
return Union(this, other, all: true);
}
}
class OrderBy {
final String key;
final bool descending;
const OrderBy(this.key, {this.descending = false});
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}) {
if (obj is DateTime) {
return withQuotes ? "'${dateYmdHms.format(obj)}'" : dateYmdHms.format(obj);
} else if (obj is bool) {
return obj ? 'TRUE' : 'FALSE';
} else if (obj == null) {
return 'NULL';
} else if (obj is String) {
var b = StringBuffer();
var escaped = false;
var it = obj.runes.iterator;
while (it.moveNext()) {
if (it.current == $nul) {
continue; // Skip null byte
} else if (it.current == $single_quote) {
escaped = true;
b.write('\\x');
b.write(it.current.toRadixString(16).padLeft(2, '0'));
} else if (isAscii(it.current)) {
b.writeCharCode(it.current);
} else if (it.currentSize == 1) {
escaped = true;
b.write('\\u');
b.write(it.current.toRadixString(16).padLeft(4, '0'));
} else if (it.currentSize == 2) {
escaped = true;
b.write('\\U');
b.write(it.current.toRadixString(16).padLeft(8, '0'));
} else {
throw UnsupportedError(
'toSql() cannot encode a rune of size (${it.currentSize})');
}
}
if (!withQuotes) {
return b.toString();
} else if (escaped) {
return "E'$b'";
} else {
return "'$b'";
}
} else {
return obj.toString();
}
}
import 'join_builder.dart';
import 'order_by.dart';
import 'query_base.dart';
import 'query_executor.dart';
import 'query_values.dart';
import 'query_where.dart';
/// A SQL `SELECT` query builder.
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
@ -424,241 +320,3 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
return update(executor).then((it) => it.isEmpty ? null : it.first);
}
}
abstract class QueryValues {
Map<String, String> get casts => {};
Map<String, dynamic> toMap();
String applyCast(String name, String sub) {
if (casts.containsKey(name)) {
var type = casts[name];
return 'CAST ($sub as $type)';
} else {
return sub;
}
}
String compileInsert(Query query, String tableName) {
var data = Map<String, dynamic>.from(toMap());
var keys = data.keys.toList();
keys.where((k) => !query.fields.contains(k)).forEach(data.remove);
if (data.isEmpty) return null;
var fieldSet = data.keys.join(', ');
var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES (');
int i = 0;
for (var entry in data.entries) {
if (i++ > 0) b.write(', ');
var name = query.reserveName(entry.key);
var s = applyCast(entry.key, '@$name');
query.substitutionValues[name] = entry.value;
b.write(s);
}
b.write(')');
return b.toString();
}
String compileForUpdate(Query query) {
var data = toMap();
if (data.isEmpty) return null;
var b = StringBuffer('SET');
int i = 0;
for (var entry in data.entries) {
if (i++ > 0) b.write(',');
b.write(' ');
b.write(entry.key);
b.write('=');
var name = query.reserveName(entry.key);
var s = applyCast(entry.key, '@$name');
query.substitutionValues[name] = entry.value;
b.write(s);
}
return b.toString();
}
}
/// A [QueryValues] implementation that simply writes to a [Map].
class MapQueryValues extends QueryValues {
final Map<String, dynamic> values = {};
@override
Map<String, dynamic> toMap() => values;
}
/// Builds a SQL `WHERE` clause.
abstract class QueryWhere {
final Set<QueryWhere> _and = Set();
final Set<QueryWhere> _not = Set();
final Set<QueryWhere> _or = Set();
Iterable<SqlExpressionBuilder> get expressionBuilders;
void and(QueryWhere other) {
_and.add(other);
}
void not(QueryWhere other) {
_not.add(other);
}
void or(QueryWhere other) {
_or.add(other);
}
String compile({String tableName}) {
var b = StringBuffer();
int i = 0;
for (var builder in expressionBuilders) {
var key = builder.columnName;
if (tableName != null) key = '$tableName.$key';
if (builder.hasValue) {
if (i++ > 0) b.write(' AND ');
if (builder is DateTimeSqlExpressionBuilder ||
(builder is JsonSqlExpressionBuilder && builder.hasRaw)) {
if (tableName != null) b.write('$tableName.');
b.write(builder.compile());
} else {
b.write('$key ${builder.compile()}');
}
}
}
for (var other in _and) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' AND $sql');
}
for (var other in _not) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' NOT $sql');
}
for (var other in _or) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' OR $sql');
}
return b.toString();
}
}
/// Represents the `UNION` of two subqueries.
class Union<T> extends QueryBase<T> {
/// The subject(s) of this binary operation.
final QueryBase<T> left, right;
/// Whether this is a `UNION ALL` operation.
final bool all;
@override
final String tableName;
Union(this.left, this.right, {this.all = false, String tableName})
: this.tableName = tableName ?? left.tableName {
substitutionValues
..addAll(left.substitutionValues)
..addAll(right.substitutionValues);
}
@override
List<String> get fields => left.fields;
@override
T deserialize(List row) => left.deserialize(row);
@override
String compile(Set<String> trampoline,
{bool includeTableName = false,
String preamble,
bool withFields = true}) {
var selector = all == true ? 'UNION ALL' : 'UNION';
var t1 = Set<String>.from(trampoline);
var t2 = Set<String>.from(trampoline);
return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})';
}
}
/// Builds a SQL `JOIN` query.
class JoinBuilder {
final JoinType type;
final Query from;
final String to, key, value, op, alias;
final List<String> additionalFields;
JoinBuilder(this.type, this.from, this.to, this.key, this.value,
{this.op = '=', this.alias, this.additionalFields = const []}) {
assert(to != null,
'computation of this join threw an error, and returned null.');
}
String get fieldName {
var right = '$to.$value';
if (alias != null) right = '$alias.$value';
return right;
}
String nameFor(String name) {
var right = '$to.$name';
if (alias != null) right = '$alias.$name';
return right;
}
String compile(Set<String> trampoline) {
if (to == null) return null;
var b = StringBuffer();
var left = '${from.tableName}.$key';
var right = fieldName;
switch (type) {
case JoinType.inner:
b.write(' INNER JOIN');
break;
case JoinType.left:
b.write(' LEFT JOIN');
break;
case JoinType.right:
b.write(' RIGHT JOIN');
break;
case JoinType.full:
b.write(' FULL OUTER JOIN');
break;
case JoinType.self:
b.write(' SELF JOIN');
break;
}
b.write(' $to');
if (alias != null) b.write(' $alias');
b.write(' ON $left$op$right');
return b.toString();
}
}
class JoinOn {
final SqlExpressionBuilder key;
final SqlExpressionBuilder value;
JoinOn(this.key, this.value);
}
/// An abstract interface that performs queries.
///
/// This class should be implemented.
abstract class QueryExecutor {
const QueryExecutor();
/// Executes a single query.
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String> returningFields]);
/// Begins a database transaction.
Future<T> transaction<T>(FutureOr<T> f());
}

View file

@ -0,0 +1,50 @@
import 'dart:async';
import 'query_executor.dart';
import 'union.dart';
/// A base class for objects that compile to SQL queries, typically within an ORM.
abstract class QueryBase<T> {
/// Casts to perform when querying the database.
Map<String, String> get casts => {};
/// Values to insert into a prepared statement.
final Map<String, dynamic> substitutionValues = {};
/// The table against which to execute this query.
String get tableName;
/// The list of fields returned by this query.
///
/// If it's `null`, then this query will perform a `SELECT *`.
List<String> get fields;
/// A String of all [fields], joined by a comma (`,`).
String get fieldSet => fields.map((k) {
var cast = casts[k];
return cast == null ? k : 'CAST ($k AS $cast)';
}).join(', ');
String compile(Set<String> trampoline,
{bool includeTableName = false, String preamble, bool withFields = true});
T deserialize(List row);
Future<List<T>> get(QueryExecutor executor) async {
var sql = compile(Set());
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.map(deserialize).toList());
}
Future<T> getOne(QueryExecutor executor) {
return get(executor).then((it) => it.isEmpty ? null : it.first);
}
Union<T> union(QueryBase<T> other) {
return Union(this, other);
}
Union<T> unionAll(QueryBase<T> other) {
return Union(this, other, all: true);
}
}

View file

@ -0,0 +1,16 @@
import 'dart:async';
/// An abstract interface that performs queries.
///
/// This class should be implemented.
abstract class QueryExecutor {
const QueryExecutor();
/// Executes a single query.
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String> returningFields]);
/// Begins a database transaction.
Future<T> transaction<T>(FutureOr<T> f());
}

View file

@ -0,0 +1,67 @@
import 'query.dart';
abstract class QueryValues {
Map<String, String> get casts => {};
Map<String, dynamic> toMap();
String applyCast(String name, String sub) {
if (casts.containsKey(name)) {
var type = casts[name];
return 'CAST ($sub as $type)';
} else {
return sub;
}
}
String compileInsert(Query query, String tableName) {
var data = Map<String, dynamic>.from(toMap());
var keys = data.keys.toList();
keys.where((k) => !query.fields.contains(k)).forEach(data.remove);
if (data.isEmpty) return null;
var fieldSet = data.keys.join(', ');
var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES (');
int i = 0;
for (var entry in data.entries) {
if (i++ > 0) b.write(', ');
var name = query.reserveName(entry.key);
var s = applyCast(entry.key, '@$name');
query.substitutionValues[name] = entry.value;
b.write(s);
}
b.write(')');
return b.toString();
}
String compileForUpdate(Query query) {
var data = toMap();
if (data.isEmpty) return null;
var b = StringBuffer('SET');
int i = 0;
for (var entry in data.entries) {
if (i++ > 0) b.write(',');
b.write(' ');
b.write(entry.key);
b.write('=');
var name = query.reserveName(entry.key);
var s = applyCast(entry.key, '@$name');
query.substitutionValues[name] = entry.value;
b.write(s);
}
return b.toString();
}
}
/// A [QueryValues] implementation that simply writes to a [Map].
class MapQueryValues extends QueryValues {
final Map<String, dynamic> values = {};
@override
Map<String, dynamic> toMap() => values;
}

View file

@ -0,0 +1,59 @@
import 'builder.dart';
/// Builds a SQL `WHERE` clause.
abstract class QueryWhere {
final Set<QueryWhere> _and = Set();
final Set<QueryWhere> _not = Set();
final Set<QueryWhere> _or = Set();
Iterable<SqlExpressionBuilder> get expressionBuilders;
void and(QueryWhere other) {
_and.add(other);
}
void not(QueryWhere other) {
_not.add(other);
}
void or(QueryWhere other) {
_or.add(other);
}
String compile({String tableName}) {
var b = StringBuffer();
int i = 0;
for (var builder in expressionBuilders) {
var key = builder.columnName;
if (tableName != null) key = '$tableName.$key';
if (builder.hasValue) {
if (i++ > 0) b.write(' AND ');
if (builder is DateTimeSqlExpressionBuilder ||
(builder is JsonSqlExpressionBuilder && builder.hasRaw)) {
if (tableName != null) b.write('$tableName.');
b.write(builder.compile());
} else {
b.write('$key ${builder.compile()}');
}
}
}
for (var other in _and) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' AND $sql');
}
for (var other in _not) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' NOT $sql');
}
for (var other in _or) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' OR $sql');
}
return b.toString();
}
}

View file

@ -0,0 +1,37 @@
import 'query_base.dart';
/// Represents the `UNION` of two subqueries.
class Union<T> extends QueryBase<T> {
/// The subject(s) of this binary operation.
final QueryBase<T> left, right;
/// Whether this is a `UNION ALL` operation.
final bool all;
@override
final String tableName;
Union(this.left, this.right, {this.all = false, String tableName})
: this.tableName = tableName ?? left.tableName {
substitutionValues
..addAll(left.substitutionValues)
..addAll(right.substitutionValues);
}
@override
List<String> get fields => left.fields;
@override
T deserialize(List row) => left.deserialize(row);
@override
String compile(Set<String> trampoline,
{bool includeTableName = false,
String preamble,
bool withFields = true}) {
var selector = all == true ? 'UNION ALL' : 'UNION';
var t1 = Set<String>.from(trampoline);
var t2 = Set<String>.from(trampoline);
return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})';
}
}

View file

@ -0,0 +1,57 @@
import 'dart:async';
import 'package:charcode/ascii.dart';
import 'annotations.dart';
import 'builder.dart';
import 'query_base.dart';
bool isAscii(int ch) => ch >= $nul && ch <= $del;
/// The ORM prefers using substitution values, which allow for prepared queries,
/// and prevent SQL injection attacks.
@deprecated
String toSql(Object obj, {bool withQuotes = true}) {
if (obj is DateTime) {
return withQuotes ? "'${dateYmdHms.format(obj)}'" : dateYmdHms.format(obj);
} else if (obj is bool) {
return obj ? 'TRUE' : 'FALSE';
} else if (obj == null) {
return 'NULL';
} else if (obj is String) {
var b = StringBuffer();
var escaped = false;
var it = obj.runes.iterator;
while (it.moveNext()) {
if (it.current == $nul) {
continue; // Skip null byte
} else if (it.current == $single_quote) {
escaped = true;
b.write('\\x');
b.write(it.current.toRadixString(16).padLeft(2, '0'));
} else if (isAscii(it.current)) {
b.writeCharCode(it.current);
} else if (it.currentSize == 1) {
escaped = true;
b.write('\\u');
b.write(it.current.toRadixString(16).padLeft(4, '0'));
} else if (it.currentSize == 2) {
escaped = true;
b.write('\\U');
b.write(it.current.toRadixString(16).padLeft(8, '0'));
} else {
throw UnsupportedError(
'toSql() cannot encode a rune of size (${it.currentSize})');
}
}
if (!withQuotes) {
return b.toString();
} else if (escaped) {
return "E'$b'";
} else {
return "'$b'";
}
} else {
return obj.toString();
}
}