mirror of https://github.com/AMT-Cheif/drift.git
Support selecting custom expressions
This commit is contained in:
parent
598fef750e
commit
7609df34f0
|
@ -73,6 +73,11 @@ abstract class Expression<D, T extends SqlType<D>> implements Component {
|
|||
"Expressions with unknown precedence shouldn't have inner expressions");
|
||||
inner.writeAroundPrecedence(ctx, precedence);
|
||||
}
|
||||
|
||||
/// Finds the runtime implementation of [T] in the provided [types].
|
||||
SqlType<D> findType(SqlTypeSystem types) {
|
||||
return types.forDartType<D>();
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to order the precedence of sql expressions so that we can avoid
|
||||
|
|
|
@ -77,6 +77,10 @@ class SimpleSelectStatement<T extends Table, D extends DataClass>
|
|||
return statement;
|
||||
}
|
||||
|
||||
JoinedSelectStatement addColumns(List<Expression> expressions) {
|
||||
return join(const [])..addColumns(expressions);
|
||||
}
|
||||
|
||||
/// Orders the result by the given clauses. The clauses coming first in the
|
||||
/// list have a higher priority, the later clauses are only considered if the
|
||||
/// first clause considers two rows to be equal.
|
||||
|
@ -116,9 +120,10 @@ String _beginOfSelect(bool distinct) {
|
|||
/// multiple entities.
|
||||
class TypedResult {
|
||||
/// Creates the result from the parsed table data.
|
||||
TypedResult(this._parsedData, this.rawData);
|
||||
TypedResult(this._parsedData, this.rawData, [this._parsedExpressions]);
|
||||
|
||||
final Map<TableInfo, dynamic> _parsedData;
|
||||
final Map<Expression, dynamic> _parsedExpressions;
|
||||
|
||||
/// The raw data contained in this row.
|
||||
final QueryRow rawData;
|
||||
|
@ -127,4 +132,13 @@ class TypedResult {
|
|||
D readTable<T extends Table, D extends DataClass>(TableInfo<T, D> table) {
|
||||
return _parsedData[table] as D;
|
||||
}
|
||||
|
||||
/// Reads a single column from an [expr].
|
||||
/// todo expand documentation
|
||||
D read<D, T extends SqlType<D>>(Expression<D, T> expr) {
|
||||
if (_parsedExpressions != null) {
|
||||
return _parsedExpressions[expr] as D;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,42 @@
|
|||
part of '../../query_builder.dart';
|
||||
|
||||
/// A `SELECT` statement that operates on more than one table.
|
||||
// this is called JoinedSelectStatement for legacy reasons - we also use it
|
||||
// when custom expressions are used as result columns. Basically, it stores
|
||||
// queries that are more complex than SimpleSelectStatement
|
||||
class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
|
||||
extends Query<FirstT, FirstD>
|
||||
with LimitContainerMixin, Selectable<TypedResult> {
|
||||
/// Whether to generate a `SELECT DISTINCT` query that will remove duplicate
|
||||
/// rows from the result set.
|
||||
final bool distinct;
|
||||
|
||||
/// Used internally by moor, users should use [SimpleSelectStatement.join]
|
||||
/// instead.
|
||||
JoinedSelectStatement(
|
||||
QueryEngine database, TableInfo<FirstT, FirstD> table, this._joins,
|
||||
[this.distinct = false])
|
||||
: super(database, table);
|
||||
: super(database, table) {
|
||||
// use all columns across all tables as result column for this query
|
||||
_selectedColumns ??=
|
||||
_tables.expand((t) => t.$columns).cast<Expression>().toList();
|
||||
}
|
||||
|
||||
/// Whether to generate a `SELECT DISTINCT` query that will remove duplicate
|
||||
/// rows from the result set.
|
||||
final bool distinct;
|
||||
final List<Join> _joins;
|
||||
|
||||
/// All columns that we're selecting from.
|
||||
List<Expression> _selectedColumns;
|
||||
|
||||
/// The `AS` aliases generated for each column that isn't from a table.
|
||||
///
|
||||
/// Each table column can be uniquely identified by its (potentially aliased)
|
||||
/// table and its name. So a column named `id` in a table called `users` would
|
||||
/// be written as `users.id AS "users.id"`. These columns will NOT be written
|
||||
/// into this map.
|
||||
///
|
||||
/// Other expressions used as columns will be included here. There just named
|
||||
/// in increasing order, so something like `AS c3`.
|
||||
final Map<Expression, String> _columnAliases = {};
|
||||
|
||||
/// The tables this select statement reads from
|
||||
@visibleForOverriding
|
||||
Set<TableInfo> get watchedTables => _tables.toSet();
|
||||
|
@ -30,27 +50,22 @@ class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
|
|||
ctx.hasMultipleTables = true;
|
||||
ctx.buffer..write(_beginOfSelect(distinct))..write(' ');
|
||||
|
||||
var isFirst = true;
|
||||
for (var table in _tables) {
|
||||
for (var column in table.$columns) {
|
||||
if (!isFirst) {
|
||||
ctx.buffer.write(', ');
|
||||
}
|
||||
|
||||
// We run into problems when two tables have a column with the same name
|
||||
// as we then wouldn't know which column is which. So, we create a
|
||||
// column alias that matches what is expected by the mapping function
|
||||
// in _getWithQuery by prefixing the table name.
|
||||
// We might switch to parsing via the index of the column in a row in
|
||||
// the future, but that's the solution for now.
|
||||
|
||||
column.writeInto(ctx);
|
||||
ctx.buffer.write(' AS "');
|
||||
column.writeInto(ctx, ignoreEscape: true);
|
||||
ctx.buffer.write('"');
|
||||
|
||||
isFirst = false;
|
||||
for (var i = 0; i < _selectedColumns.length; i++) {
|
||||
if (i != 0) {
|
||||
ctx.buffer.write(', ');
|
||||
}
|
||||
|
||||
final column = _selectedColumns[i];
|
||||
String chosenAlias;
|
||||
if (column is GeneratedColumn) {
|
||||
chosenAlias = '${column.tableName}.${column.$name}';
|
||||
} else {
|
||||
chosenAlias = 'c$i';
|
||||
_columnAliases[column] = chosenAlias;
|
||||
}
|
||||
|
||||
column.writeInto(ctx);
|
||||
ctx.buffer..write(' AS "')..write(chosenAlias)..write('"');
|
||||
}
|
||||
|
||||
ctx.buffer.write(' FROM ${table.tableWithAlias}');
|
||||
|
@ -94,6 +109,18 @@ class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
|
|||
orderByExpr = OrderBy(terms);
|
||||
}
|
||||
|
||||
/// Adds a custom expression to the query.
|
||||
///
|
||||
/// The database will evaluate the [Expression] for each row found for this
|
||||
/// query. The value of the expression can be extracted from the [TypedResult]
|
||||
/// by passing it to [TypedResult.read].
|
||||
///
|
||||
/// See also:
|
||||
/// - The docs on expressions: https://moor.simonbinder.eu/docs/getting-started/expressions/
|
||||
void addColumns(Iterable<Expression> expressions) {
|
||||
_selectedColumns.addAll(expressions);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<TypedResult>> watch() {
|
||||
final ctx = constructQuery();
|
||||
|
@ -131,19 +158,28 @@ class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
|
|||
final tables = _tables;
|
||||
|
||||
return results.map((row) {
|
||||
final map = <TableInfo, dynamic>{};
|
||||
final readTables = <TableInfo, dynamic>{};
|
||||
final readColumns = <Expression, dynamic>{};
|
||||
|
||||
for (var table in tables) {
|
||||
for (final table in tables) {
|
||||
final prefix = '${table.$tableName}.';
|
||||
// if all columns of this table are null, skip the table
|
||||
if (table.$columns.any((c) => row[prefix + c.$name] != null)) {
|
||||
map[table] = table.map(row, tablePrefix: table.$tableName);
|
||||
readTables[table] = table.map(row, tablePrefix: table.$tableName);
|
||||
} else {
|
||||
map[table] = null;
|
||||
readTables[table] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return TypedResult(map, QueryRow(row, database));
|
||||
for (final aliasedColumn in _columnAliases.entries) {
|
||||
final expr = aliasedColumn.key;
|
||||
final value = row[aliasedColumn.value];
|
||||
|
||||
final type = expr.findType(ctx.typeSystem);
|
||||
readColumns[expr] = type.mapFromDatabaseResponse(value);
|
||||
}
|
||||
|
||||
return TypedResult(readTables, QueryRow(row, database), readColumns);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
|
|
@ -116,6 +116,31 @@ void main() {
|
|||
argThat(contains('WHERE t.id < ? ORDER BY t.title ASC')), [3]));
|
||||
});
|
||||
|
||||
test('supports custom columns and results', () async {
|
||||
final categories = db.alias(db.categories, 'c');
|
||||
final descriptionLength = categories.description.length;
|
||||
|
||||
final query = db.select(categories).addColumns([descriptionLength]);
|
||||
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
||||
return [
|
||||
{'c.id': 3, 'c.desc': 'Description', 'c2': 11}
|
||||
];
|
||||
});
|
||||
|
||||
final result = await query.getSingle();
|
||||
|
||||
verify(executor.runSelect(
|
||||
'SELECT c.id AS "c.id", c.`desc` AS "c.desc", LENGTH(c.`desc`) AS "c2" '
|
||||
'FROM categories c;',
|
||||
[],
|
||||
));
|
||||
|
||||
expect(result.readTable(categories),
|
||||
equals(Category(id: 3, description: 'Description')));
|
||||
expect(result.read(descriptionLength), 11);
|
||||
});
|
||||
|
||||
test('injects custom error message when a table is used multiple times',
|
||||
() async {
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) => Future.error('nah'));
|
||||
|
|
|
@ -12,21 +12,26 @@ environment:
|
|||
sdk: '>=2.2.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
# todo it looks like we're not using any apis removed in 0.40.0, but I couldn't verify that (neither analyzer_plugin
|
||||
# nor build supports 0.40.0 yet)
|
||||
analyzer: '>=0.36.4 <0.40.0'
|
||||
analyzer_plugin: '>=0.1.0 <0.3.0'
|
||||
collection: ^1.14.0
|
||||
recase: ^2.0.1
|
||||
source_gen: ^0.9.4
|
||||
source_span: ^1.5.5
|
||||
build: ^1.1.0
|
||||
logging: '>=0.11.0 <1.0.0'
|
||||
build_config: '>=0.3.1 <1.0.0'
|
||||
moor: ^2.0.1
|
||||
meta: ^1.1.0
|
||||
path: ^1.6.0
|
||||
logging: '>=0.11.0 <1.0.0'
|
||||
|
||||
# Moor-specific analysis
|
||||
moor: ^2.0.1
|
||||
sqlparser: ^0.4.0
|
||||
|
||||
# Dart analysis
|
||||
analyzer: '>=0.36.4 <0.40.0'
|
||||
analyzer_plugin: '>=0.1.0 <0.3.0'
|
||||
source_span: ^1.5.5
|
||||
|
||||
# Build system
|
||||
build: ^1.1.0
|
||||
build_config: '>=0.3.1 <1.0.0'
|
||||
source_gen: ^0.9.4
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.6.0
|
||||
test_core: ^0.2.0
|
||||
|
|
Loading…
Reference in New Issue