mirror of https://github.com/AMT-Cheif/drift.git
Redo runtime api with regards to type safety and fun
This commit is contained in:
parent
2a69573330
commit
9090ded541
|
@ -1,5 +1,4 @@
|
|||
import 'package:sally/sally.dart';
|
||||
import 'package:sally/src/queries/table_structure.dart';
|
||||
|
||||
part 'example.g.dart';
|
||||
|
||||
|
@ -18,9 +17,10 @@ class Users extends Table {
|
|||
}
|
||||
|
||||
@UseData(tables: [Products, Users])
|
||||
class ShopDb extends SallyDb with _$ShopDbMixin {
|
||||
class ShopDb extends _$ShopDb {
|
||||
ShopDb(SqlTypeSystem typeSystem, QueryExecutor executor) : super(typeSystem, executor);
|
||||
|
||||
Future<List<User>> allUsers() => users.select().get();
|
||||
Future<User> userByName(String name) => (users.select()..where((u) => u.name.equals(name))).single();
|
||||
Future<List<User>> allUsers() => select(users).get();
|
||||
Future<List<User>> userByName(String name) => (select(users)..where((u) => u.name.equalsVal(name))).get();
|
||||
|
||||
}
|
|
@ -1,37 +1,10 @@
|
|||
part of 'example.dart';
|
||||
|
||||
class _$ShopDbMixin implements QueryExecutor {
|
||||
class _$ShopDb extends GeneratedDatabase {
|
||||
|
||||
final StructuredUsersTable users = StructuredUsersTable();
|
||||
|
||||
Future<List<Map<String, dynamic>>> executeQuery(String sql, [dynamic params]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<int> executeDelete(String sql, [dynamic params]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StructuredUsersTable extends Users with TableStructure<Users, User> {
|
||||
|
||||
|
||||
@override
|
||||
final StructuredIntColumn id = StructuredIntColumn("id");
|
||||
@override
|
||||
final StructuredTextColumn name = StructuredTextColumn("name");
|
||||
|
||||
@override
|
||||
String get sqlTableName => "users";
|
||||
|
||||
@override
|
||||
User parse(Map<String, dynamic> result) {
|
||||
return User(result["id"], result["name"]);
|
||||
}
|
||||
@override
|
||||
Users get asTable => this;
|
||||
_$ShopDb(SqlTypeSystem typeSystem, QueryExecutor executor) : super(typeSystem, executor);
|
||||
|
||||
UsersTable get users => null;
|
||||
}
|
||||
|
||||
class User {
|
||||
|
@ -42,3 +15,34 @@ class User {
|
|||
User(this.id, this.name);
|
||||
|
||||
}
|
||||
|
||||
class UsersTable extends Users implements TableInfo<Users, User> {
|
||||
|
||||
final GeneratedDatabase db;
|
||||
|
||||
UsersTable(this.db);
|
||||
|
||||
@override
|
||||
List<Column> get $columns => [id, name];
|
||||
|
||||
@override
|
||||
String get $tableName => "users";
|
||||
|
||||
@override
|
||||
IntColumn get id => GeneratedIntColumn("id");
|
||||
|
||||
@override
|
||||
TextColumn get name => GeneratedTextColumn("name");
|
||||
|
||||
@override
|
||||
Users get asDslTable => this;
|
||||
|
||||
@override
|
||||
User map(Map<String, dynamic> data) {
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
final stringType = db.typeSystem.forDartType<String>();
|
||||
|
||||
return User(intType.mapFromDatabaseResponse(data["id"]), stringType.mapFromDatabaseResponse(data["name"]));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,4 +2,13 @@ library sally;
|
|||
|
||||
export 'package:sally/src/dsl/table.dart';
|
||||
export 'package:sally/src/dsl/columns.dart';
|
||||
export 'package:sally/src/database.dart';
|
||||
export 'package:sally/src/dsl/database.dart';
|
||||
|
||||
export 'package:sally/src/runtime/executor/executor.dart';
|
||||
export 'package:sally/src/runtime/executor/type_system.dart';
|
||||
export 'package:sally/src/runtime/expressions/user_api.dart';
|
||||
export 'package:sally/src/runtime/statements/query.dart';
|
||||
export 'package:sally/src/runtime/statements/select.dart';
|
||||
export 'package:sally/src/runtime/structure/columns.dart';
|
||||
export 'package:sally/src/runtime/structure/table_info.dart';
|
||||
export 'package:sally/src/runtime/sql_types.dart';
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
class UseData {
|
||||
final List<Type> tables;
|
||||
final int schemaVersion;
|
||||
|
||||
const UseData({this.tables, this.schemaVersion = 1});
|
||||
}
|
||||
|
||||
abstract class QueryExecutor {
|
||||
Future<List<Map<String, dynamic>>> executeQuery(String sql, [dynamic params]);
|
||||
Future<int> executeDelete(String sql, [dynamic params]);
|
||||
}
|
||||
|
||||
abstract class SallyDb {}
|
|
@ -1,24 +1,23 @@
|
|||
// todo more datatypes (at least DateTime and Binary blobs)!
|
||||
// todo nullability
|
||||
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
class Column<T> {
|
||||
Predicate equals(T compare) => null;
|
||||
abstract class Column<T, S extends SqlType<T>> extends Expression<S> {
|
||||
Expression<BoolType> equals(Expression<S> compare);
|
||||
Expression<BoolType> equalsVal(T compare);
|
||||
}
|
||||
|
||||
abstract class IntColumn extends Column<int> {
|
||||
Predicate isBiggerThan(int i);
|
||||
Predicate isSmallerThan(int i);
|
||||
abstract class IntColumn extends Column<int, IntType> {
|
||||
Expression<BoolType> isBiggerThan(int i);
|
||||
Expression<BoolType> isSmallerThan(int i);
|
||||
}
|
||||
|
||||
abstract class BoolColumn extends Column<bool> {
|
||||
Predicate isTrue();
|
||||
Predicate isFalse();
|
||||
}
|
||||
abstract class BoolColumn extends Column<bool, BoolType> {}
|
||||
|
||||
abstract class TextColumn extends Column<String> {
|
||||
Predicate like(String regex);
|
||||
abstract class TextColumn extends Column<String, StringType> {
|
||||
Expression<BoolType> like(String regex);
|
||||
}
|
||||
|
||||
class ColumnBuilder<T> {
|
||||
|
@ -29,7 +28,7 @@ class ColumnBuilder<T> {
|
|||
ColumnBuilder<T> primaryKey() => this;
|
||||
// ColumnBuilder<T> references<Table>(Column<T> extractor(Table table)) => this;
|
||||
|
||||
Column<T> call() => null;
|
||||
Column<T, dynamic> call() => null;
|
||||
}
|
||||
|
||||
class IntColumnBuilder extends ColumnBuilder<int> {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class UseData {
|
||||
final List<Type> tables;
|
||||
final int schemaVersion;
|
||||
|
||||
const UseData({this.tables, this.schemaVersion = 1});
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
export 'package:sally/src/queries/generation_context.dart';
|
||||
export 'package:sally/src/queries/expressions/limit.dart';
|
||||
export 'package:sally/src/queries/expressions/variable.dart';
|
||||
export 'package:sally/src/queries/expressions/where.dart';
|
||||
|
||||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
|
||||
abstract class SqlExpression {
|
||||
void writeInto(GenerationContext context);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
|
||||
class LimitExpression extends SqlExpression {
|
||||
final int amount;
|
||||
final int offset;
|
||||
|
||||
LimitExpression(this.amount, this.offset);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
if (offset != null)
|
||||
context.buffer.write('LIMIT $amount, $offset ');
|
||||
else
|
||||
context.buffer.write('LIMIT $amount ');
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
|
||||
class Variable extends SqlExpression {
|
||||
final dynamic value;
|
||||
|
||||
Variable(this.value);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.addBoundVariable(value);
|
||||
|
||||
context.buffer.write('? ');
|
||||
}
|
||||
}
|
||||
|
||||
class HardcodedConstant extends SqlExpression {
|
||||
|
||||
final dynamic value;
|
||||
|
||||
HardcodedConstant(this.value);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write(context.harcodedSqlValue(value));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
|
||||
class WhereExpression extends SqlExpression {
|
||||
final Predicate predicate;
|
||||
|
||||
WhereExpression(this.predicate);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write("WHERE ");
|
||||
predicate.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
class GenerationContext {
|
||||
StringBuffer buffer = StringBuffer();
|
||||
List<dynamic> boundVariables = List();
|
||||
|
||||
void addBoundVariable(dynamic data) {
|
||||
boundVariables.add(data);
|
||||
}
|
||||
|
||||
String harcodedSqlValue(dynamic value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import 'package:sally/src/queries/generation_context.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
|
||||
class NotPredicate extends Predicate {
|
||||
final Predicate inner;
|
||||
|
||||
NotPredicate(this.inner);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write("NOT ");
|
||||
inner.writeInto(context);
|
||||
}
|
||||
}
|
||||
|
||||
class OrPredicate extends Predicate {
|
||||
final Predicate a, b;
|
||||
|
||||
OrPredicate(this.a, this.b);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('(');
|
||||
a.writeInto(context);
|
||||
context.buffer.write(') OR ( ');
|
||||
b.writeInto(context);
|
||||
context.buffer.write(') ');
|
||||
}
|
||||
}
|
||||
|
||||
class AndPredicate extends Predicate {
|
||||
final Predicate a, b;
|
||||
|
||||
AndPredicate(this.a, this.b);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('(');
|
||||
a.writeInto(context);
|
||||
context.buffer.write(') AND (');
|
||||
b.writeInto(context);
|
||||
context.buffer.write(') ');
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
|
||||
enum ComparisonOperator { less, less_or_equal, more, more_or_equal }
|
||||
|
||||
class NumberComparisonPredicate extends Predicate {
|
||||
static const Map<ComparisonOperator, String> _operators = {
|
||||
ComparisonOperator.less: '< ',
|
||||
ComparisonOperator.less_or_equal: '<= ',
|
||||
ComparisonOperator.more: '> ',
|
||||
ComparisonOperator.more_or_equal: '>= ',
|
||||
};
|
||||
|
||||
SqlExpression left;
|
||||
ComparisonOperator operator;
|
||||
SqlExpression right;
|
||||
|
||||
NumberComparisonPredicate(this.left, this.operator, this.right);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
left.writeInto(context);
|
||||
context.buffer.write(_operators[operator]);
|
||||
right.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
export 'package:sally/src/queries/predicates/combining.dart';
|
||||
export 'package:sally/src/queries/predicates/numbers.dart';
|
||||
export 'package:sally/src/queries/predicates/text.dart';
|
||||
|
||||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
import 'package:sally/src/queries/predicates/combining.dart';
|
||||
|
||||
Predicate not(Predicate p) => p.not();
|
||||
|
||||
abstract class Predicate extends SqlExpression {
|
||||
Predicate not() {
|
||||
return NotPredicate(this);
|
||||
}
|
||||
|
||||
Predicate and(Predicate other) => AndPredicate(this, other);
|
||||
Predicate or(Predicate other) => OrPredicate(this, other);
|
||||
}
|
||||
|
||||
class EqualityPredicate extends Predicate {
|
||||
SqlExpression left;
|
||||
SqlExpression right;
|
||||
|
||||
EqualityPredicate(this.left, this.right);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
left.writeInto(context);
|
||||
context.buffer.write('= ');
|
||||
right.writeInto(context);
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanExpressionPredicate extends Predicate {
|
||||
SqlExpression expression;
|
||||
|
||||
BooleanExpressionPredicate(this.expression);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
expression.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
|
||||
class LikePredicate extends Predicate {
|
||||
SqlExpression target;
|
||||
SqlExpression regex;
|
||||
|
||||
LikePredicate(this.target, this.regex);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
target.writeInto(context);
|
||||
context.buffer.write('LIKE ');
|
||||
regex.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/statement/statements.dart';
|
||||
import 'package:sally/src/queries/table_structure.dart';
|
||||
|
||||
class DeleteStatement<Table> with Limitable, WhereFilterable<Table, dynamic> {
|
||||
|
||||
DeleteStatement(TableStructure<Table, dynamic> table) {
|
||||
super.table = table;
|
||||
}
|
||||
|
||||
/// Deletes all records matched by the optional where and limit statements.
|
||||
/// Returns the amount of deleted rows.
|
||||
Future<int> performDelete() {
|
||||
GenerationContext context = GenerationContext();
|
||||
context.buffer.write('DELETE FROM ');
|
||||
context.buffer.write(table.sqlTableName);
|
||||
context.buffer.write(' ');
|
||||
|
||||
if (hasWhere) whereExpression.writeInto(context);
|
||||
if (hasLimit) limitExpression.writeInto(context);
|
||||
|
||||
return table.executor.executeDelete(context.buffer.toString(), context.boundVariables);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/statement/statements.dart';
|
||||
import 'package:sally/src/queries/table_structure.dart';
|
||||
|
||||
class SelectStatement<Table, Result> with Limitable, WhereFilterable<Table, Result> {
|
||||
|
||||
SelectStatement(TableStructure<Table, Result> table) {
|
||||
super.table = table;
|
||||
}
|
||||
|
||||
GenerationContext _buildQuery() {
|
||||
GenerationContext context = GenerationContext();
|
||||
context.buffer.write('SELECT * FROM ');
|
||||
context.buffer.write(table.sqlTableName);
|
||||
context.buffer.write(' ');
|
||||
|
||||
if (hasWhere) whereExpression.writeInto(context);
|
||||
if (hasLimit) limitExpression.writeInto(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// Executes the select statement on the database and maps the returned rows
|
||||
/// to the right dataclass.
|
||||
Future<List<Result>> get() async {
|
||||
final ctx = _buildQuery();
|
||||
final sql = ctx.buffer.toString();
|
||||
final vars = ctx.boundVariables;
|
||||
|
||||
final result = await table.executor.executeQuery(sql, vars);
|
||||
return result.map(table.parse).toList();
|
||||
}
|
||||
|
||||
/// Similar to [get], but it will only load one item by setting [limit()]
|
||||
/// appropriately. This method will throw if no results where found. If you're
|
||||
/// ok with no result existing, try [singleOrNull] instead.
|
||||
Future<Result> single() async {
|
||||
final element = singleOrNull();
|
||||
if (element == null)
|
||||
throw StateError("No item was returned by the query called with single()");
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/// Similar to [get], but only uses one row of the result by setting the limit
|
||||
/// accordingly. If no item was found, null will be returned instead.
|
||||
Future<Result> singleOrNull() async {
|
||||
// limit to one item, using the existing offset if it exists
|
||||
limitExpression = LimitExpression(1, limitExpression.offset ?? 0);
|
||||
|
||||
final results = await get();
|
||||
if (results.isEmpty)
|
||||
return null;
|
||||
return results.single;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/expressions/limit.dart';
|
||||
import 'package:sally/src/queries/expressions/where.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
import 'package:sally/src/queries/table_structure.dart';
|
||||
|
||||
/// Mixin for statements that allow a LIMIT operator
|
||||
class Limitable {
|
||||
|
||||
@protected
|
||||
LimitExpression limitExpression;
|
||||
|
||||
void limit({int amount, int offset}) {
|
||||
limitExpression = LimitExpression(amount, offset);
|
||||
}
|
||||
|
||||
@protected
|
||||
bool get hasLimit => limitExpression != null;
|
||||
|
||||
}
|
||||
|
||||
/// Mixin for statements that allow a WHERE operator on a specific table.
|
||||
class WhereFilterable<Table, Result> {
|
||||
|
||||
@protected
|
||||
TableStructure<Table, Result> table;
|
||||
@protected
|
||||
WhereExpression whereExpression;
|
||||
|
||||
bool get hasWhere => whereExpression != null;
|
||||
|
||||
void where(Predicate filter(Table table)) {
|
||||
final addedPredicate = filter(table.asTable);
|
||||
|
||||
if (hasWhere) {
|
||||
// merge existing where expression together with new one by and-ing them
|
||||
// together.
|
||||
whereExpression = WhereExpression(whereExpression.predicate.and(addedPredicate));
|
||||
} else {
|
||||
whereExpression = WhereExpression(addedPredicate);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import 'package:sally/sally.dart';
|
||||
import 'package:sally/src/dsl/columns.dart';
|
||||
import 'package:sally/src/queries/expressions/expressions.dart';
|
||||
import 'package:sally/src/queries/expressions/variable.dart';
|
||||
import 'package:sally/src/queries/generation_context.dart';
|
||||
import 'package:sally/src/queries/predicates/numbers.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
import 'package:sally/src/queries/predicates/text.dart';
|
||||
import 'package:sally/src/queries/statement/delete.dart';
|
||||
import 'package:sally/src/queries/statement/select.dart';
|
||||
|
||||
abstract class TableStructure<UserSpecifiedTable, ResolvedType> {
|
||||
QueryExecutor executor;
|
||||
|
||||
UserSpecifiedTable get asTable;
|
||||
String get sqlTableName;
|
||||
|
||||
ResolvedType parse(Map<String, dynamic> result);
|
||||
|
||||
SelectStatement<UserSpecifiedTable, ResolvedType> select() =>
|
||||
SelectStatement<UserSpecifiedTable, ResolvedType>(this);
|
||||
|
||||
DeleteStatement<UserSpecifiedTable> delete() => DeleteStatement(this);
|
||||
}
|
||||
|
||||
class StructuredColumn<T> implements SqlExpression, Column<T> {
|
||||
final String sqlName;
|
||||
|
||||
StructuredColumn(this.sqlName);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
// todo table name lookup, as-expressions etc?
|
||||
context.buffer.write(sqlName);
|
||||
context.buffer.write(' ');
|
||||
}
|
||||
|
||||
@override
|
||||
Predicate equals(T compare) => EqualityPredicate(this, Variable(compare));
|
||||
}
|
||||
|
||||
class StructuredIntColumn extends StructuredColumn<int> implements IntColumn {
|
||||
StructuredIntColumn(String sqlName) : super(sqlName);
|
||||
|
||||
@override
|
||||
Predicate isBiggerThan(int i) =>
|
||||
NumberComparisonPredicate(this, ComparisonOperator.more, Variable(i));
|
||||
@override
|
||||
Predicate isSmallerThan(int i) =>
|
||||
NumberComparisonPredicate(this, ComparisonOperator.less, Variable(i));
|
||||
}
|
||||
|
||||
class StructuredBoolColumn extends StructuredColumn<bool>
|
||||
implements BoolColumn {
|
||||
StructuredBoolColumn(String sqlName) : super(sqlName);
|
||||
|
||||
// Booleans will be stored as integers, where 0 means false and 1 means true
|
||||
|
||||
@override
|
||||
Predicate isFalse() {
|
||||
return EqualityPredicate(this, HardcodedConstant(0));
|
||||
}
|
||||
@override
|
||||
Predicate isTrue() {
|
||||
return EqualityPredicate(this, HardcodedConstant(1));
|
||||
}
|
||||
}
|
||||
|
||||
class StructuredTextColumn extends StructuredColumn<String>
|
||||
implements TextColumn {
|
||||
StructuredTextColumn(String sqlName) : super(sqlName);
|
||||
|
||||
@override
|
||||
Predicate like(String regex) => LikePredicate(this, Variable(regex));
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:sally/src/runtime/executor/executor.dart';
|
||||
|
||||
/// Anything that can appear in a sql query.
|
||||
abstract class Component {
|
||||
/// Writes this component into the [context] by writing to its
|
||||
/// [GenerationContext.buffer] or by introducing bound variables.
|
||||
void writeInto(GenerationContext context);
|
||||
}
|
||||
|
||||
/// Contains information about a query while it's being constructed.
|
||||
class GenerationContext {
|
||||
final GeneratedDatabase database;
|
||||
|
||||
final List<dynamic> _boundVariables = [];
|
||||
List<dynamic> get boundVariables => _boundVariables;
|
||||
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
|
||||
String get sql => buffer.toString();
|
||||
|
||||
GenerationContext(this.database);
|
||||
|
||||
void introduceVariable(dynamic value) {
|
||||
_boundVariables.add(value);
|
||||
}
|
||||
|
||||
void writeWhitespace() => buffer.write(' ');
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:sally/src/runtime/components/component.dart';
|
||||
|
||||
class Limit extends Component {
|
||||
final int amount;
|
||||
final int offset;
|
||||
|
||||
Limit(this.amount, this.offset);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
if (offset != null)
|
||||
context.buffer.write('LIMIT $amount, $offset');
|
||||
else
|
||||
context.buffer.write('LIMIT $amount');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
class Where extends Component {
|
||||
final Expression<BoolType> predicate;
|
||||
|
||||
Where(this.predicate);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write("WHERE ");
|
||||
predicate.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:sally/sally.dart';
|
||||
import 'package:sally/src/runtime/executor/type_system.dart';
|
||||
import 'package:sally/src/runtime/statements/select.dart';
|
||||
|
||||
/// A base class for all generated databases.
|
||||
abstract class GeneratedDatabase {
|
||||
final SqlTypeSystem typeSystem;
|
||||
final QueryExecutor executor;
|
||||
|
||||
GeneratedDatabase(this.typeSystem, this.executor);
|
||||
|
||||
SelectStatement<Table, ReturnType> select<Table, ReturnType>(
|
||||
TableInfo<Table, ReturnType> table) {
|
||||
return SelectStatement<Table, ReturnType>(this, table);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class QueryExecutor {
|
||||
Future<bool> ensureOpen();
|
||||
Future<List<Map<String, dynamic>>> runSelect(
|
||||
String statement, List<dynamic> args);
|
||||
List<int> runCreate(String statement, List<dynamic> args);
|
||||
Future<int> runUpdate(String statement, List<dynamic> args);
|
||||
Future<int> runDelete(String statement, List<dynamic> args);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
class SqlTypeSystem {
|
||||
final List<SqlType> types;
|
||||
|
||||
const SqlTypeSystem(this.types);
|
||||
|
||||
const SqlTypeSystem.withDefaults()
|
||||
: this(const [BoolType(), StringType(), IntType()]);
|
||||
|
||||
/// Returns the appropriate sql type for the dart type provided as the
|
||||
/// generic parameter.
|
||||
SqlType<T> forDartType<T>() {
|
||||
return types.singleWhere((t) => t is SqlType<T>);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
Expression<BoolType> and(Expression<BoolType> a, Expression<BoolType> b) =>
|
||||
AndExpression(a, b);
|
||||
|
||||
Expression<BoolType> not(Expression<BoolType> a) => NotExpression(a);
|
||||
|
||||
class AndExpression extends Expression<BoolType> with InfixOperator<BoolType> {
|
||||
Expression<BoolType> left, right;
|
||||
|
||||
final String operator = "AND";
|
||||
|
||||
AndExpression(this.left, this.right);
|
||||
}
|
||||
|
||||
class OrExpression extends Expression<BoolType> with InfixOperator<BoolType> {
|
||||
Expression<BoolType> left, right;
|
||||
|
||||
final String operator = "AND";
|
||||
|
||||
OrExpression(this.left, this.right);
|
||||
}
|
||||
|
||||
class NotExpression extends Expression<BoolType> {
|
||||
Expression<BoolType> inner;
|
||||
|
||||
NotExpression(this.inner);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('NOT ');
|
||||
inner.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
/// Any sql expression that evaluates to some generic value. This does not
|
||||
/// include queries (which might evaluate to multiple values) but individual
|
||||
/// columns, functions and operators.
|
||||
abstract class Expression<T extends SqlType> implements Component {}
|
||||
|
||||
/// An expression that looks like "$a operator $b$, where $a and $b itself
|
||||
/// are expressions and operator is any string.
|
||||
abstract class InfixOperator<T extends SqlType> implements Expression<T> {
|
||||
Expression get left;
|
||||
Expression get right;
|
||||
String get operator;
|
||||
|
||||
@visibleForOverriding
|
||||
bool get placeBrackets => true;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
_placeBracketIfNeeded(context, true);
|
||||
|
||||
left.writeInto(context);
|
||||
|
||||
_placeBracketIfNeeded(context, false);
|
||||
context.writeWhitespace();
|
||||
context.buffer.write(operator);
|
||||
context.writeWhitespace();
|
||||
_placeBracketIfNeeded(context, true);
|
||||
|
||||
right.writeInto(context);
|
||||
|
||||
_placeBracketIfNeeded(context, false);
|
||||
}
|
||||
|
||||
void _placeBracketIfNeeded(GenerationContext context, bool open) {
|
||||
if (placeBrackets) context.buffer.write(open ? '(' : ')');
|
||||
}
|
||||
}
|
||||
|
||||
enum ComparisonOperator { less, less_or_equal, equal, more_or_equal, more }
|
||||
|
||||
class Comparison extends InfixOperator<BoolType> {
|
||||
static const Map<ComparisonOperator, String> operatorNames = {
|
||||
ComparisonOperator.less: '<',
|
||||
ComparisonOperator.less_or_equal: '<=',
|
||||
ComparisonOperator.equal: '=',
|
||||
ComparisonOperator.more_or_equal: '>=',
|
||||
ComparisonOperator.more: '>'
|
||||
};
|
||||
|
||||
final Expression left;
|
||||
final Expression right;
|
||||
final ComparisonOperator op;
|
||||
|
||||
final bool placeBrackets = false;
|
||||
|
||||
@override
|
||||
String get operator => operatorNames[op];
|
||||
|
||||
Comparison(this.left, this.op, this.right);
|
||||
|
||||
Comparison.equal(this.left, this.right) : this.op = ComparisonOperator.equal;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
class LikeOperator extends Expression<BoolType> {
|
||||
final Expression<StringType> target;
|
||||
final Expression<StringType> regex;
|
||||
|
||||
LikeOperator(this.target, this.regex);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
target.writeInto(context);
|
||||
context.buffer.write(' LIKE ');
|
||||
regex.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export 'bools.dart' show and, not;
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
class Variable<T, S extends SqlType<T>> extends Expression<S> {
|
||||
final T value;
|
||||
|
||||
Variable(this.value);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.introduceVariable(value);
|
||||
context.buffer.write("?");
|
||||
}
|
||||
}
|
||||
|
||||
class Constant<T, S extends SqlType<T>> extends Expression<S> {
|
||||
final T value;
|
||||
|
||||
Constant(this.value);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
final type = context.database.typeSystem.forDartType<T>();
|
||||
context.buffer.write(type.mapToSqlConstant(value));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/// A type that can be mapped from Dart to sql. The generic type parameter here
|
||||
/// denotes the resolved dart type.
|
||||
abstract class SqlType<T> {
|
||||
const SqlType();
|
||||
|
||||
/// Maps the [content] to a value that we can send together with a prepared
|
||||
/// statement to represent the given value.
|
||||
dynamic mapToSqlVariable(T content);
|
||||
|
||||
/// Maps the given content to a sql literal that can be included in the query
|
||||
/// string.
|
||||
String mapToSqlConstant(T content);
|
||||
|
||||
/// Maps the response from sql back to a readable dart type.
|
||||
T mapFromDatabaseResponse(dynamic response);
|
||||
}
|
||||
|
||||
/// A mapper for boolean values in sql. Booleans are represented as integers,
|
||||
/// where 0 means false and any other value means true.
|
||||
class BoolType extends SqlType<bool> {
|
||||
const BoolType();
|
||||
|
||||
@override
|
||||
bool mapFromDatabaseResponse(response) {
|
||||
return !(response == 0);
|
||||
}
|
||||
|
||||
@override
|
||||
String mapToSqlConstant(bool content) {
|
||||
return content ? "1" : "0";
|
||||
}
|
||||
|
||||
@override
|
||||
mapToSqlVariable(bool content) {
|
||||
return content ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
class StringType extends SqlType<String> {
|
||||
const StringType();
|
||||
|
||||
@override
|
||||
String mapFromDatabaseResponse(response) => response;
|
||||
|
||||
@override
|
||||
String mapToSqlConstant(String content) {
|
||||
// TODO: implement mapToSqlConstant
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
mapToSqlVariable(String content) => content;
|
||||
}
|
||||
|
||||
class IntType extends SqlType<int> {
|
||||
const IntType();
|
||||
|
||||
@override
|
||||
int mapFromDatabaseResponse(response) => response;
|
||||
|
||||
@override
|
||||
String mapToSqlConstant(int content) => content.toString();
|
||||
|
||||
@override
|
||||
mapToSqlVariable(int content) {
|
||||
return content;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/components/limit.dart';
|
||||
import 'package:sally/src/runtime/components/where.dart';
|
||||
import 'package:sally/src/runtime/executor/executor.dart';
|
||||
import 'package:sally/src/runtime/expressions/bools.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
import 'package:sally/src/runtime/structure/table_info.dart';
|
||||
|
||||
/// Statement that operates on a table (select, update, insert, delete).
|
||||
abstract class Query<Table> {
|
||||
@protected
|
||||
GeneratedDatabase database;
|
||||
@protected
|
||||
TableInfo<Table, dynamic> table;
|
||||
|
||||
Query(this.database, this.table);
|
||||
|
||||
@protected
|
||||
Where whereExpr;
|
||||
@protected
|
||||
Limit limitExpr;
|
||||
|
||||
void writeStartPart(GenerationContext ctx);
|
||||
|
||||
void where(Expression<BoolType> filter(Table tbl)) {
|
||||
final predicate = filter(table.asDslTable);
|
||||
|
||||
if (whereExpr == null) {
|
||||
whereExpr = Where(predicate);
|
||||
} else {
|
||||
whereExpr = Where(and(whereExpr.predicate, predicate));
|
||||
}
|
||||
}
|
||||
|
||||
void limit(int limit, {int offset}) {
|
||||
limitExpr = Limit(limit, offset);
|
||||
}
|
||||
|
||||
@protected
|
||||
GenerationContext constructQuery() {
|
||||
final ctx = GenerationContext(database);
|
||||
var needsWhitespace = false;
|
||||
|
||||
writeStartPart(ctx);
|
||||
needsWhitespace = true;
|
||||
|
||||
if (whereExpr != null) {
|
||||
if (needsWhitespace) ctx.writeWhitespace();
|
||||
|
||||
whereExpr.writeInto(ctx);
|
||||
needsWhitespace = true;
|
||||
}
|
||||
|
||||
if (limitExpr != null) {
|
||||
if (needsWhitespace) ctx.writeWhitespace();
|
||||
|
||||
limitExpr.writeInto(ctx);
|
||||
needsWhitespace = true;
|
||||
}
|
||||
|
||||
ctx.buffer.write(';');
|
||||
|
||||
return ctx;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/executor/executor.dart';
|
||||
import 'package:sally/src/runtime/statements/query.dart';
|
||||
import 'package:sally/src/runtime/structure/table_info.dart';
|
||||
|
||||
class SelectStatement<UserTable, DataType> extends Query<UserTable> {
|
||||
@override
|
||||
covariant TableInfo<UserTable, DataType> table;
|
||||
|
||||
SelectStatement(GeneratedDatabase database, this.table)
|
||||
: super(database, table);
|
||||
|
||||
@override
|
||||
void writeStartPart(GenerationContext ctx) {
|
||||
ctx.buffer.write('SELECT * FROM ${table.$tableName}');
|
||||
}
|
||||
|
||||
/// Loads and returns all results from this select query.
|
||||
Future<List<DataType>> get() async {
|
||||
final ctx = constructQuery();
|
||||
|
||||
final results =
|
||||
await ctx.database.executor.runSelect(ctx.sql, ctx.boundVariables);
|
||||
return results.map(table.map).toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:sally/sally.dart';
|
||||
import 'package:sally/src/runtime/components/component.dart';
|
||||
import 'package:sally/src/runtime/expressions/expression.dart';
|
||||
import 'package:sally/src/runtime/expressions/text.dart';
|
||||
import 'package:sally/src/runtime/expressions/variables.dart';
|
||||
import 'package:sally/src/runtime/sql_types.dart';
|
||||
|
||||
abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
|
||||
String get $name;
|
||||
|
||||
@override
|
||||
Expression<BoolType> equals(Expression<S> compare) =>
|
||||
Comparison.equal(this, compare);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write($name);
|
||||
}
|
||||
|
||||
@override
|
||||
Expression<BoolType> equalsVal(T compare) => equals(Variable<T, S>(compare));
|
||||
}
|
||||
|
||||
class GeneratedTextColumn extends GeneratedColumn<String, StringType>
|
||||
implements TextColumn {
|
||||
final String $name;
|
||||
|
||||
GeneratedTextColumn(this.$name);
|
||||
|
||||
@override
|
||||
Expression<BoolType> like(String regex) =>
|
||||
LikeOperator(this, Variable<String, StringType>(regex));
|
||||
}
|
||||
|
||||
class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
|
||||
implements BoolColumn {
|
||||
final String $name;
|
||||
|
||||
GeneratedBoolColumn(this.$name);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('(');
|
||||
context.buffer.write($name);
|
||||
context.buffer.write(' = 1)');
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratedIntColumn extends GeneratedColumn<int, IntType>
|
||||
implements IntColumn {
|
||||
final String $name;
|
||||
|
||||
GeneratedIntColumn(this.$name);
|
||||
|
||||
@override
|
||||
Expression<BoolType> isBiggerThan(int i) =>
|
||||
Comparison(this, ComparisonOperator.more, Variable<int, IntType>(i));
|
||||
|
||||
@override
|
||||
Expression<BoolType> isSmallerThan(int i) =>
|
||||
Comparison(this, ComparisonOperator.less, Variable<int, IntType>(i));
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:sally/sally.dart';
|
||||
|
||||
/// Base class for generated classes.
|
||||
abstract class TableInfo<TableDsl, DataClass> {
|
||||
TableDsl get asDslTable;
|
||||
|
||||
/// The table name in the sql table
|
||||
String get $tableName;
|
||||
List<Column> get $columns;
|
||||
|
||||
DataClass map(Map<String, dynamic> data);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:sally/sally.dart';
|
||||
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text().withLength(min: 6, max: 32)();
|
||||
BoolColumn get isAwesome => boolean()();
|
||||
}
|
||||
|
||||
// Example tables and data classes, these would be generated by sally_generator
|
||||
// in a real project
|
||||
class UserDataObject {
|
||||
final int id;
|
||||
final String name;
|
||||
UserDataObject(this.id, this.name);
|
||||
}
|
||||
|
||||
class GeneratedUsersTable extends Users with TableInfo<Users, UserDataObject> {
|
||||
final GeneratedDatabase db;
|
||||
|
||||
GeneratedUsersTable(this.db);
|
||||
|
||||
IntColumn id = GeneratedIntColumn("id");
|
||||
TextColumn name = GeneratedTextColumn("name");
|
||||
BoolColumn isAwesome = GeneratedBoolColumn("is_awesome");
|
||||
@override
|
||||
List<Column<dynamic, SqlType>> get $columns => [id, name, isAwesome];
|
||||
@override
|
||||
String get $tableName => "users";
|
||||
@override
|
||||
Users get asDslTable => this;
|
||||
@override
|
||||
UserDataObject map(Map<String, dynamic> data) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDatabase extends GeneratedDatabase {
|
||||
TestDatabase(QueryExecutor executor)
|
||||
: super(SqlTypeSystem.withDefaults(), executor);
|
||||
|
||||
GeneratedUsersTable get users => GeneratedUsersTable(this);
|
||||
}
|
|
@ -1,95 +1,65 @@
|
|||
import 'package:sally/sally.dart';
|
||||
import 'package:sally/src/queries/predicates/predicate.dart';
|
||||
import 'package:sally/src/queries/table_structure.dart';
|
||||
import 'package:test_api/test_api.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'generated_tables.dart';
|
||||
|
||||
class MockExecutor extends Mock implements QueryExecutor {}
|
||||
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text().withLength(min: 6, max: 32)();
|
||||
BoolColumn get isAwesome => boolean()();
|
||||
}
|
||||
|
||||
// Example tables and data classes, these would be generated by sally_generator
|
||||
// in a real project
|
||||
class UserDataObject {
|
||||
final int id;
|
||||
final String name;
|
||||
UserDataObject(this.id, this.name);
|
||||
}
|
||||
|
||||
class GeneratedUsersTable extends Users
|
||||
with TableStructure<Users, UserDataObject> {
|
||||
@override
|
||||
Users get asTable => this;
|
||||
@override
|
||||
UserDataObject parse(Map<String, dynamic> result) {
|
||||
return UserDataObject(result["id"], result["name"]);
|
||||
}
|
||||
|
||||
@override
|
||||
String get sqlTableName => "users";
|
||||
|
||||
IntColumn id = StructuredIntColumn("id");
|
||||
TextColumn name = StructuredTextColumn("name");
|
||||
BoolColumn isAwesome = StructuredBoolColumn("is_awesome");
|
||||
}
|
||||
|
||||
void main() {
|
||||
GeneratedUsersTable users;
|
||||
TestDatabase db;
|
||||
MockExecutor executor;
|
||||
|
||||
setUp(() {
|
||||
users = GeneratedUsersTable();
|
||||
executor = MockExecutor();
|
||||
users.executor = executor;
|
||||
db = TestDatabase(executor);
|
||||
|
||||
when(executor.executeQuery(any, any)).thenAnswer((_) => Future.value([]));
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([]));
|
||||
});
|
||||
|
||||
group("Generates SELECT statements", () {
|
||||
test("generates simple statements", () {
|
||||
users.select().get();
|
||||
verify(executor.executeQuery("SELECT * FROM users ", any));
|
||||
db.select(db.users).get();
|
||||
verify(executor.runSelect("SELECT * FROM users;", argThat(isEmpty)));
|
||||
});
|
||||
|
||||
test("generates limit statements", () {
|
||||
(users.select()..limit(amount: 10)).get();
|
||||
verify(executor.executeQuery("SELECT * FROM users LIMIT 10 ", any));
|
||||
(db.select(db.users)..limit(10)).get();
|
||||
verify(executor.runSelect(
|
||||
"SELECT * FROM users LIMIT 10;", argThat(isEmpty)));
|
||||
});
|
||||
|
||||
test("generates like expressions", () {
|
||||
(users.select()..where((u) => u.name.like("Dash%"))).get();
|
||||
(db.select(db.users)..where((u) => u.name.like("Dash%"))).get();
|
||||
verify(executor
|
||||
.executeQuery("SELECT * FROM users WHERE name LIKE ? ", ["Dash%"]));
|
||||
.runSelect("SELECT * FROM users WHERE name LIKE ?;", ["Dash%"]));
|
||||
});
|
||||
|
||||
test("generates complex predicates", () {
|
||||
(users.select()
|
||||
..where(
|
||||
(u) => not(u.name.equals("Dash")).and(u.id.isBiggerThan(12))))
|
||||
(db.select(db.users)
|
||||
..where((u) =>
|
||||
and(not(u.name.equalsVal("Dash")), (u.id.isBiggerThan(12)))))
|
||||
.get();
|
||||
|
||||
verify(executor.executeQuery(
|
||||
"SELECT * FROM users WHERE (NOT name = ? ) AND (id > ? ) ",
|
||||
verify(executor.runSelect(
|
||||
"SELECT * FROM users WHERE (NOT name = ?) AND (id > ?);",
|
||||
["Dash", 12]));
|
||||
});
|
||||
|
||||
test("generates expressions from boolean fields", () {
|
||||
(users.select()..where((u) => u.isAwesome.isTrue())).get();
|
||||
(db.select(db.users)..where((u) => u.isAwesome)).get();
|
||||
|
||||
verify(executor.executeQuery(
|
||||
"SELECT * FROM users WHERE is_awesome = 1", any));
|
||||
verify(executor.runSelect(
|
||||
"SELECT * FROM users WHERE (is_awesome = 1);", argThat(isEmpty)));
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
group("Generates DELETE statements", () {
|
||||
test("without any constaints", () {
|
||||
test("without any constraints", () {
|
||||
users.delete().performDelete();
|
||||
|
||||
verify(executor.executeDelete("DELETE FROM users ", argThat(isEmpty)));
|
||||
});
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue