Redo runtime api with regards to type safety and fun

This commit is contained in:
Simon Binder 2019-02-04 20:40:49 +01:00
parent 2a69573330
commit 9090ded541
36 changed files with 613 additions and 531 deletions

View File

@ -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();
}

View File

@ -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 {
@ -41,4 +14,35 @@ 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"]));
}
}

View File

@ -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';

View File

@ -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 {}

View File

@ -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> {

View File

@ -0,0 +1,6 @@
class UseData {
final List<Type> tables;
final int schemaVersion;
const UseData({this.tables, this.schemaVersion = 1});
}

View File

@ -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);
}

View File

@ -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 ');
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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(') ');
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}

View File

@ -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(' ');
}

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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>);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
export 'bools.dart' show and, not;

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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));
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)));
});
});
});*/
}