From 9090ded54142d50863ff8b199e8386d88b1c31eb Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 4 Feb 2019 20:40:49 +0100 Subject: [PATCH] Redo runtime api with regards to type safety and fun --- example/lib/example.dart | 8 +- example/lib/example.g.dart | 66 ++++++++-------- sally/lib/sally.dart | 11 ++- sally/lib/src/database.dart | 13 ---- sally/lib/src/dsl/columns.dart | 25 +++--- sally/lib/src/dsl/database.dart | 6 ++ .../src/queries/expressions/expressions.dart | 10 --- sally/lib/src/queries/expressions/limit.dart | 17 ----- .../lib/src/queries/expressions/variable.dart | 28 ------- sally/lib/src/queries/expressions/where.dart | 15 ---- sally/lib/src/queries/generation_context.dart | 12 --- .../lib/src/queries/predicates/combining.dart | 44 ----------- sally/lib/src/queries/predicates/numbers.dart | 27 ------- .../lib/src/queries/predicates/predicate.dart | 43 ----------- sally/lib/src/queries/predicates/text.dart | 17 ----- sally/lib/src/queries/statement/delete.dart | 25 ------ sally/lib/src/queries/statement/select.dart | 58 -------------- .../lib/src/queries/statement/statements.dart | 45 ----------- sally/lib/src/queries/table_structure.dart | 75 ------------------ .../lib/src/runtime/components/component.dart | 28 +++++++ sally/lib/src/runtime/components/limit.dart | 16 ++++ sally/lib/src/runtime/components/where.dart | 15 ++++ sally/lib/src/runtime/executor/executor.dart | 25 ++++++ .../lib/src/runtime/executor/type_system.dart | 16 ++++ sally/lib/src/runtime/expressions/bools.dart | 36 +++++++++ .../src/runtime/expressions/expression.dart | 65 ++++++++++++++++ sally/lib/src/runtime/expressions/text.dart | 17 +++++ .../lib/src/runtime/expressions/user_api.dart | 1 + .../src/runtime/expressions/variables.dart | 27 +++++++ sally/lib/src/runtime/sql_types.dart | 68 +++++++++++++++++ sally/lib/src/runtime/statements/query.dart | 67 ++++++++++++++++ sally/lib/src/runtime/statements/select.dart | 26 +++++++ sally/lib/src/runtime/structure/columns.dart | 62 +++++++++++++++ .../lib/src/runtime/structure/table_info.dart | 12 +++ sally/test/generated_tables.dart | 42 ++++++++++ sally/test/queries_test.dart | 76 ++++++------------- 36 files changed, 613 insertions(+), 531 deletions(-) delete mode 100644 sally/lib/src/database.dart create mode 100644 sally/lib/src/dsl/database.dart delete mode 100644 sally/lib/src/queries/expressions/expressions.dart delete mode 100644 sally/lib/src/queries/expressions/limit.dart delete mode 100644 sally/lib/src/queries/expressions/variable.dart delete mode 100644 sally/lib/src/queries/expressions/where.dart delete mode 100644 sally/lib/src/queries/generation_context.dart delete mode 100644 sally/lib/src/queries/predicates/combining.dart delete mode 100644 sally/lib/src/queries/predicates/numbers.dart delete mode 100644 sally/lib/src/queries/predicates/predicate.dart delete mode 100644 sally/lib/src/queries/predicates/text.dart delete mode 100644 sally/lib/src/queries/statement/delete.dart delete mode 100644 sally/lib/src/queries/statement/select.dart delete mode 100644 sally/lib/src/queries/statement/statements.dart delete mode 100644 sally/lib/src/queries/table_structure.dart create mode 100644 sally/lib/src/runtime/components/component.dart create mode 100644 sally/lib/src/runtime/components/limit.dart create mode 100644 sally/lib/src/runtime/components/where.dart create mode 100644 sally/lib/src/runtime/executor/executor.dart create mode 100644 sally/lib/src/runtime/executor/type_system.dart create mode 100644 sally/lib/src/runtime/expressions/bools.dart create mode 100644 sally/lib/src/runtime/expressions/expression.dart create mode 100644 sally/lib/src/runtime/expressions/text.dart create mode 100644 sally/lib/src/runtime/expressions/user_api.dart create mode 100644 sally/lib/src/runtime/expressions/variables.dart create mode 100644 sally/lib/src/runtime/sql_types.dart create mode 100644 sally/lib/src/runtime/statements/query.dart create mode 100644 sally/lib/src/runtime/statements/select.dart create mode 100644 sally/lib/src/runtime/structure/columns.dart create mode 100644 sally/lib/src/runtime/structure/table_info.dart create mode 100644 sally/test/generated_tables.dart diff --git a/example/lib/example.dart b/example/lib/example.dart index 3b6df024..95bed61b 100644 --- a/example/lib/example.dart +++ b/example/lib/example.dart @@ -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> allUsers() => users.select().get(); - Future userByName(String name) => (users.select()..where((u) => u.name.equals(name))).single(); + Future> allUsers() => select(users).get(); + Future> userByName(String name) => (select(users)..where((u) => u.name.equalsVal(name))).get(); } \ No newline at end of file diff --git a/example/lib/example.g.dart b/example/lib/example.g.dart index 5aa2b93f..7d68cbff 100644 --- a/example/lib/example.g.dart +++ b/example/lib/example.g.dart @@ -1,37 +1,10 @@ part of 'example.dart'; -class _$ShopDbMixin implements QueryExecutor { +class _$ShopDb extends GeneratedDatabase { - final StructuredUsersTable users = StructuredUsersTable(); - - Future>> executeQuery(String sql, [dynamic params]) { - return null; - } - - Future executeDelete(String sql, [dynamic params]) { - return null; - } - -} - -class StructuredUsersTable extends Users with TableStructure { - - - @override - final StructuredIntColumn id = StructuredIntColumn("id"); - @override - final StructuredTextColumn name = StructuredTextColumn("name"); - - @override - String get sqlTableName => "users"; - - @override - User parse(Map 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 { @@ -41,4 +14,35 @@ class User { User(this.id, this.name); -} \ No newline at end of file +} + +class UsersTable extends Users implements TableInfo { + + final GeneratedDatabase db; + + UsersTable(this.db); + + @override + List 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 data) { + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + + return User(intType.mapFromDatabaseResponse(data["id"]), stringType.mapFromDatabaseResponse(data["name"])); + } + +} diff --git a/sally/lib/sally.dart b/sally/lib/sally.dart index 57ec7022..0632ee70 100644 --- a/sally/lib/sally.dart +++ b/sally/lib/sally.dart @@ -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'; diff --git a/sally/lib/src/database.dart b/sally/lib/src/database.dart deleted file mode 100644 index 049115b3..00000000 --- a/sally/lib/src/database.dart +++ /dev/null @@ -1,13 +0,0 @@ -class UseData { - final List tables; - final int schemaVersion; - - const UseData({this.tables, this.schemaVersion = 1}); -} - -abstract class QueryExecutor { - Future>> executeQuery(String sql, [dynamic params]); - Future executeDelete(String sql, [dynamic params]); -} - -abstract class SallyDb {} diff --git a/sally/lib/src/dsl/columns.dart b/sally/lib/src/dsl/columns.dart index a3a51206..a2843ecf 100644 --- a/sally/lib/src/dsl/columns.dart +++ b/sally/lib/src/dsl/columns.dart @@ -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 { - Predicate equals(T compare) => null; +abstract class Column> extends Expression { + Expression equals(Expression compare); + Expression equalsVal(T compare); } -abstract class IntColumn extends Column { - Predicate isBiggerThan(int i); - Predicate isSmallerThan(int i); +abstract class IntColumn extends Column { + Expression isBiggerThan(int i); + Expression isSmallerThan(int i); } -abstract class BoolColumn extends Column { - Predicate isTrue(); - Predicate isFalse(); -} +abstract class BoolColumn extends Column {} -abstract class TextColumn extends Column { - Predicate like(String regex); +abstract class TextColumn extends Column { + Expression like(String regex); } class ColumnBuilder { @@ -29,7 +28,7 @@ class ColumnBuilder { ColumnBuilder primaryKey() => this; // ColumnBuilder references(Column extractor(Table table)) => this; - Column call() => null; + Column call() => null; } class IntColumnBuilder extends ColumnBuilder { diff --git a/sally/lib/src/dsl/database.dart b/sally/lib/src/dsl/database.dart new file mode 100644 index 00000000..6f27f5e7 --- /dev/null +++ b/sally/lib/src/dsl/database.dart @@ -0,0 +1,6 @@ +class UseData { + final List tables; + final int schemaVersion; + + const UseData({this.tables, this.schemaVersion = 1}); +} diff --git a/sally/lib/src/queries/expressions/expressions.dart b/sally/lib/src/queries/expressions/expressions.dart deleted file mode 100644 index b7faeb81..00000000 --- a/sally/lib/src/queries/expressions/expressions.dart +++ /dev/null @@ -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); -} diff --git a/sally/lib/src/queries/expressions/limit.dart b/sally/lib/src/queries/expressions/limit.dart deleted file mode 100644 index 468d16c3..00000000 --- a/sally/lib/src/queries/expressions/limit.dart +++ /dev/null @@ -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 '); - } -} diff --git a/sally/lib/src/queries/expressions/variable.dart b/sally/lib/src/queries/expressions/variable.dart deleted file mode 100644 index c0fd464a..00000000 --- a/sally/lib/src/queries/expressions/variable.dart +++ /dev/null @@ -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)); - } - -} diff --git a/sally/lib/src/queries/expressions/where.dart b/sally/lib/src/queries/expressions/where.dart deleted file mode 100644 index ae54991d..00000000 --- a/sally/lib/src/queries/expressions/where.dart +++ /dev/null @@ -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); - } -} diff --git a/sally/lib/src/queries/generation_context.dart b/sally/lib/src/queries/generation_context.dart deleted file mode 100644 index 22d9f142..00000000 --- a/sally/lib/src/queries/generation_context.dart +++ /dev/null @@ -1,12 +0,0 @@ -class GenerationContext { - StringBuffer buffer = StringBuffer(); - List boundVariables = List(); - - void addBoundVariable(dynamic data) { - boundVariables.add(data); - } - - String harcodedSqlValue(dynamic value) { - return value.toString(); - } -} diff --git a/sally/lib/src/queries/predicates/combining.dart b/sally/lib/src/queries/predicates/combining.dart deleted file mode 100644 index 74bfc457..00000000 --- a/sally/lib/src/queries/predicates/combining.dart +++ /dev/null @@ -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(') '); - } -} diff --git a/sally/lib/src/queries/predicates/numbers.dart b/sally/lib/src/queries/predicates/numbers.dart deleted file mode 100644 index 645ac8b3..00000000 --- a/sally/lib/src/queries/predicates/numbers.dart +++ /dev/null @@ -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 _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); - } -} diff --git a/sally/lib/src/queries/predicates/predicate.dart b/sally/lib/src/queries/predicates/predicate.dart deleted file mode 100644 index b8a8f184..00000000 --- a/sally/lib/src/queries/predicates/predicate.dart +++ /dev/null @@ -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); - } -} diff --git a/sally/lib/src/queries/predicates/text.dart b/sally/lib/src/queries/predicates/text.dart deleted file mode 100644 index 728b09a2..00000000 --- a/sally/lib/src/queries/predicates/text.dart +++ /dev/null @@ -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); - } -} diff --git a/sally/lib/src/queries/statement/delete.dart b/sally/lib/src/queries/statement/delete.dart deleted file mode 100644 index 2c09b814..00000000 --- a/sally/lib/src/queries/statement/delete.dart +++ /dev/null @@ -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
with Limitable, WhereFilterable { - - DeleteStatement(TableStructure table) { - super.table = table; - } - - /// Deletes all records matched by the optional where and limit statements. - /// Returns the amount of deleted rows. - Future 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); - } - -} \ No newline at end of file diff --git a/sally/lib/src/queries/statement/select.dart b/sally/lib/src/queries/statement/select.dart deleted file mode 100644 index 168c1494..00000000 --- a/sally/lib/src/queries/statement/select.dart +++ /dev/null @@ -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 with Limitable, WhereFilterable { - - SelectStatement(TableStructure 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> 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 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 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; - } - -} - diff --git a/sally/lib/src/queries/statement/statements.dart b/sally/lib/src/queries/statement/statements.dart deleted file mode 100644 index 4832d156..00000000 --- a/sally/lib/src/queries/statement/statements.dart +++ /dev/null @@ -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 { - - @protected - TableStructure 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); - } - } - -} \ No newline at end of file diff --git a/sally/lib/src/queries/table_structure.dart b/sally/lib/src/queries/table_structure.dart deleted file mode 100644 index e3c2f759..00000000 --- a/sally/lib/src/queries/table_structure.dart +++ /dev/null @@ -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 { - QueryExecutor executor; - - UserSpecifiedTable get asTable; - String get sqlTableName; - - ResolvedType parse(Map result); - - SelectStatement select() => - SelectStatement(this); - - DeleteStatement delete() => DeleteStatement(this); -} - -class StructuredColumn implements SqlExpression, Column { - 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 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 - 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 - implements TextColumn { - StructuredTextColumn(String sqlName) : super(sqlName); - - @override - Predicate like(String regex) => LikePredicate(this, Variable(regex)); -} diff --git a/sally/lib/src/runtime/components/component.dart b/sally/lib/src/runtime/components/component.dart new file mode 100644 index 00000000..f9a4c7fb --- /dev/null +++ b/sally/lib/src/runtime/components/component.dart @@ -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 _boundVariables = []; + List 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(' '); +} diff --git a/sally/lib/src/runtime/components/limit.dart b/sally/lib/src/runtime/components/limit.dart new file mode 100644 index 00000000..d380310f --- /dev/null +++ b/sally/lib/src/runtime/components/limit.dart @@ -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'); + } +} diff --git a/sally/lib/src/runtime/components/where.dart b/sally/lib/src/runtime/components/where.dart new file mode 100644 index 00000000..2838c89a --- /dev/null +++ b/sally/lib/src/runtime/components/where.dart @@ -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 predicate; + + Where(this.predicate); + + @override + void writeInto(GenerationContext context) { + context.buffer.write("WHERE "); + predicate.writeInto(context); + } +} diff --git a/sally/lib/src/runtime/executor/executor.dart b/sally/lib/src/runtime/executor/executor.dart new file mode 100644 index 00000000..6ad7953f --- /dev/null +++ b/sally/lib/src/runtime/executor/executor.dart @@ -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 select( + TableInfo table) { + return SelectStatement(this, table); + } +} + +abstract class QueryExecutor { + Future ensureOpen(); + Future>> runSelect( + String statement, List args); + List runCreate(String statement, List args); + Future runUpdate(String statement, List args); + Future runDelete(String statement, List args); +} diff --git a/sally/lib/src/runtime/executor/type_system.dart b/sally/lib/src/runtime/executor/type_system.dart new file mode 100644 index 00000000..99a9c182 --- /dev/null +++ b/sally/lib/src/runtime/executor/type_system.dart @@ -0,0 +1,16 @@ +import 'package:sally/src/runtime/sql_types.dart'; + +class SqlTypeSystem { + final List 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 forDartType() { + return types.singleWhere((t) => t is SqlType); + } +} diff --git a/sally/lib/src/runtime/expressions/bools.dart b/sally/lib/src/runtime/expressions/bools.dart new file mode 100644 index 00000000..65537263 --- /dev/null +++ b/sally/lib/src/runtime/expressions/bools.dart @@ -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 and(Expression a, Expression b) => + AndExpression(a, b); + +Expression not(Expression a) => NotExpression(a); + +class AndExpression extends Expression with InfixOperator { + Expression left, right; + + final String operator = "AND"; + + AndExpression(this.left, this.right); +} + +class OrExpression extends Expression with InfixOperator { + Expression left, right; + + final String operator = "AND"; + + OrExpression(this.left, this.right); +} + +class NotExpression extends Expression { + Expression inner; + + NotExpression(this.inner); + + @override + void writeInto(GenerationContext context) { + context.buffer.write('NOT '); + inner.writeInto(context); + } +} diff --git a/sally/lib/src/runtime/expressions/expression.dart b/sally/lib/src/runtime/expressions/expression.dart new file mode 100644 index 00000000..e37c5814 --- /dev/null +++ b/sally/lib/src/runtime/expressions/expression.dart @@ -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 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 implements Expression { + 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 { + static const Map 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; +} diff --git a/sally/lib/src/runtime/expressions/text.dart b/sally/lib/src/runtime/expressions/text.dart new file mode 100644 index 00000000..96043288 --- /dev/null +++ b/sally/lib/src/runtime/expressions/text.dart @@ -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 { + final Expression target; + final Expression regex; + + LikeOperator(this.target, this.regex); + + @override + void writeInto(GenerationContext context) { + target.writeInto(context); + context.buffer.write(' LIKE '); + regex.writeInto(context); + } +} diff --git a/sally/lib/src/runtime/expressions/user_api.dart b/sally/lib/src/runtime/expressions/user_api.dart new file mode 100644 index 00000000..7f062358 --- /dev/null +++ b/sally/lib/src/runtime/expressions/user_api.dart @@ -0,0 +1 @@ +export 'bools.dart' show and, not; diff --git a/sally/lib/src/runtime/expressions/variables.dart b/sally/lib/src/runtime/expressions/variables.dart new file mode 100644 index 00000000..16e43661 --- /dev/null +++ b/sally/lib/src/runtime/expressions/variables.dart @@ -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> extends Expression { + final T value; + + Variable(this.value); + + @override + void writeInto(GenerationContext context) { + context.introduceVariable(value); + context.buffer.write("?"); + } +} + +class Constant> extends Expression { + final T value; + + Constant(this.value); + + @override + void writeInto(GenerationContext context) { + final type = context.database.typeSystem.forDartType(); + context.buffer.write(type.mapToSqlConstant(value)); + } +} diff --git a/sally/lib/src/runtime/sql_types.dart b/sally/lib/src/runtime/sql_types.dart new file mode 100644 index 00000000..e0605475 --- /dev/null +++ b/sally/lib/src/runtime/sql_types.dart @@ -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 { + 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 { + 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 { + 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 { + const IntType(); + + @override + int mapFromDatabaseResponse(response) => response; + + @override + String mapToSqlConstant(int content) => content.toString(); + + @override + mapToSqlVariable(int content) { + return content; + } +} diff --git a/sally/lib/src/runtime/statements/query.dart b/sally/lib/src/runtime/statements/query.dart new file mode 100644 index 00000000..6e5ceed4 --- /dev/null +++ b/sally/lib/src/runtime/statements/query.dart @@ -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
{ + @protected + GeneratedDatabase database; + @protected + TableInfo table; + + Query(this.database, this.table); + + @protected + Where whereExpr; + @protected + Limit limitExpr; + + void writeStartPart(GenerationContext ctx); + + void where(Expression 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; + } +} diff --git a/sally/lib/src/runtime/statements/select.dart b/sally/lib/src/runtime/statements/select.dart new file mode 100644 index 00000000..9c7a8596 --- /dev/null +++ b/sally/lib/src/runtime/statements/select.dart @@ -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 extends Query { + @override + covariant TableInfo 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> get() async { + final ctx = constructQuery(); + + final results = + await ctx.database.executor.runSelect(ctx.sql, ctx.boundVariables); + return results.map(table.map).toList(); + } +} diff --git a/sally/lib/src/runtime/structure/columns.dart b/sally/lib/src/runtime/structure/columns.dart new file mode 100644 index 00000000..cca4be9a --- /dev/null +++ b/sally/lib/src/runtime/structure/columns.dart @@ -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> extends Column { + String get $name; + + @override + Expression equals(Expression compare) => + Comparison.equal(this, compare); + + @override + void writeInto(GenerationContext context) { + context.buffer.write($name); + } + + @override + Expression equalsVal(T compare) => equals(Variable(compare)); +} + +class GeneratedTextColumn extends GeneratedColumn + implements TextColumn { + final String $name; + + GeneratedTextColumn(this.$name); + + @override + Expression like(String regex) => + LikeOperator(this, Variable(regex)); +} + +class GeneratedBoolColumn extends GeneratedColumn + 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 + implements IntColumn { + final String $name; + + GeneratedIntColumn(this.$name); + + @override + Expression isBiggerThan(int i) => + Comparison(this, ComparisonOperator.more, Variable(i)); + + @override + Expression isSmallerThan(int i) => + Comparison(this, ComparisonOperator.less, Variable(i)); +} diff --git a/sally/lib/src/runtime/structure/table_info.dart b/sally/lib/src/runtime/structure/table_info.dart new file mode 100644 index 00000000..1086a85b --- /dev/null +++ b/sally/lib/src/runtime/structure/table_info.dart @@ -0,0 +1,12 @@ +import 'package:sally/sally.dart'; + +/// Base class for generated classes. +abstract class TableInfo { + TableDsl get asDslTable; + + /// The table name in the sql table + String get $tableName; + List get $columns; + + DataClass map(Map data); +} diff --git a/sally/test/generated_tables.dart b/sally/test/generated_tables.dart new file mode 100644 index 00000000..cb2ca882 --- /dev/null +++ b/sally/test/generated_tables.dart @@ -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 { + final GeneratedDatabase db; + + GeneratedUsersTable(this.db); + + IntColumn id = GeneratedIntColumn("id"); + TextColumn name = GeneratedTextColumn("name"); + BoolColumn isAwesome = GeneratedBoolColumn("is_awesome"); + @override + List> get $columns => [id, name, isAwesome]; + @override + String get $tableName => "users"; + @override + Users get asDslTable => this; + @override + UserDataObject map(Map data) { + return null; + } +} + +class TestDatabase extends GeneratedDatabase { + TestDatabase(QueryExecutor executor) + : super(SqlTypeSystem.withDefaults(), executor); + + GeneratedUsersTable get users => GeneratedUsersTable(this); +} diff --git a/sally/test/queries_test.dart b/sally/test/queries_test.dart index 07b3944b..ecac340e 100644 --- a/sally/test/queries_test.dart +++ b/sally/test/queries_test.dart @@ -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 { - @override - Users get asTable => this; - @override - UserDataObject parse(Map 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))); }); - }); + });*/ }