Implement delete

This commit is contained in:
Simon Binder 2019-02-03 16:53:20 +01:00
parent 64ceb2acfa
commit 2a69573330
12 changed files with 189 additions and 91 deletions

View File

@ -21,6 +21,6 @@ class Users extends Table {
class ShopDb extends SallyDb with _$ShopDbMixin {
Future<List<User>> allUsers() => users.select().get();
Future<User> userByName(String name) => users.select().where((u) => u.name.equals(name)).single();
Future<User> userByName(String name) => (users.select()..where((u) => u.name.equals(name))).single();
}

View File

@ -8,6 +8,10 @@ class _$ShopDbMixin implements QueryExecutor {
return null;
}
Future<int> executeDelete(String sql, [dynamic params]) {
return null;
}
}
class StructuredUsersTable extends Users with TableStructure<Users, User> {

View File

@ -7,6 +7,7 @@ class UseData {
abstract class QueryExecutor {
Future<List<Map<String, dynamic>>> executeQuery(String sql, [dynamic params]);
Future<int> executeDelete(String sql, [dynamic params]);
}
abstract class SallyDb {}

View File

@ -7,18 +7,18 @@ class Column<T> {
Predicate equals(T compare) => null;
}
class IntColumn extends Column<int> {
Predicate isBiggerThan(int i) => null;
Predicate isSmallerThan(int i) => null;
abstract class IntColumn extends Column<int> {
Predicate isBiggerThan(int i);
Predicate isSmallerThan(int i);
}
class BoolColumn extends Column<bool> {
Predicate isTrue() => null;
Predicate isFalse() => null;
abstract class BoolColumn extends Column<bool> {
Predicate isTrue();
Predicate isFalse();
}
class TextColumn extends Column<String> {
Predicate like(String regex) => null;
abstract class TextColumn extends Column<String> {
Predicate like(String regex);
}
class ColumnBuilder<T> {

View File

@ -13,3 +13,16 @@ class Variable extends SqlExpression {
context.buffer.write('? ');
}
}
class HardcodedConstant extends SqlExpression {
final dynamic value;
HardcodedConstant(this.value);
@override
void writeInto(GenerationContext context) {
context.buffer.write(context.harcodedSqlValue(value));
}
}

View File

@ -5,4 +5,8 @@ class GenerationContext {
void addBoundVariable(dynamic data) {
boundVariables.add(data);
}
String harcodedSqlValue(dynamic value) {
return value.toString();
}
}

View File

@ -0,0 +1,25 @@
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);
}
}

View File

@ -0,0 +1,58 @@
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;
}
}

View File

@ -0,0 +1,45 @@
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);
}
}
}

View File

@ -1,71 +0,0 @@
import 'package:sally/src/queries/expressions/limit.dart';
import 'package:sally/src/queries/expressions/where.dart';
import 'package:sally/src/queries/generation_context.dart';
import 'package:sally/src/queries/predicates/predicate.dart';
import 'package:sally/src/queries/table_structure.dart';
abstract class SqlStatement {
GenerationContext _buildQuery();
}
abstract class StatementForExistingData<Table, Result> extends SqlStatement {
final TableStructure<Table, Result> _table;
StatementForExistingData(this._table);
WhereExpression _where;
LimitExpression _limit;
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();
}
Future<Result> single() async {
// limit to one item, using the existing offset if it exists
_limit = LimitExpression(1, _limit?.offset ?? 0);
return (await get()).single;
}
StatementForExistingData<Table, Result> where(
Predicate extractor(Table tbl)) {
final addedPredicate = extractor(_table.asTable);
if (_where != null) {
// merge existing where expression together with new one by and-ing them
// together.
_where = WhereExpression(_where.predicate.and(addedPredicate));
} else {
_where = WhereExpression(addedPredicate);
}
return this;
}
StatementForExistingData<Table, Result> limit({int amount, int offset}) {
_limit = LimitExpression(amount, offset);
return this;
}
}
class SelectStatement<T, R> extends StatementForExistingData<T, R> {
SelectStatement(TableStructure<T, R> table) : super(table);
@override
GenerationContext _buildQuery() {
GenerationContext context = GenerationContext();
context.buffer.write('SELECT * FROM ');
context.buffer.write(_table.sqlTableName);
context.buffer.write(' ');
if (_where != null) _where.writeInto(context);
if (_limit != null) _limit.writeInto(context);
return context;
}
}

View File

@ -6,7 +6,8 @@ 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/statements.dart';
import 'package:sally/src/queries/statement/delete.dart';
import 'package:sally/src/queries/statement/select.dart';
abstract class TableStructure<UserSpecifiedTable, ResolvedType> {
QueryExecutor executor;
@ -18,6 +19,8 @@ abstract class TableStructure<UserSpecifiedTable, ResolvedType> {
SelectStatement<UserSpecifiedTable, ResolvedType> select() =>
SelectStatement<UserSpecifiedTable, ResolvedType>(this);
DeleteStatement<UserSpecifiedTable> delete() => DeleteStatement(this);
}
class StructuredColumn<T> implements SqlExpression, Column<T> {
@ -51,14 +54,15 @@ class StructuredBoolColumn extends StructuredColumn<bool>
implements BoolColumn {
StructuredBoolColumn(String sqlName) : super(sqlName);
@override
Predicate isFalse() {
return not(isTrue());
}
// 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 BooleanExpressionPredicate(this);
return EqualityPredicate(this, HardcodedConstant(1));
}
}

View File

@ -56,25 +56,40 @@ void main() {
});
test("generates limit statements", () {
users.select().limit(amount: 10).get();
(users.select()..limit(amount: 10)).get();
verify(executor.executeQuery("SELECT * FROM users LIMIT 10 ", any));
});
test("generates like expressions", () {
users.select().where((u) => u.name.like("Dash%")).get();
(users.select()..where((u) => u.name.like("Dash%"))).get();
verify(executor
.executeQuery("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)))
(users.select()
..where(
(u) => not(u.name.equals("Dash")).and(u.id.isBiggerThan(12))))
.get();
verify(executor.executeQuery(
"SELECT * FROM users WHERE (NOT name = ? ) AND (id > ? ) ",
["Dash", 12]));
});
test("generates expressions from boolean fields", () {
(users.select()..where((u) => u.isAwesome.isTrue())).get();
verify(executor.executeQuery(
"SELECT * FROM users WHERE is_awesome = 1", any));
});
});
group("Generates DELETE statements", () {
test("without any constaints", () {
users.delete().performDelete();
verify(executor.executeDelete("DELETE FROM users ", argThat(isEmpty)));
});
});
}