From 5e4ae6e568ae1a9ddc84e4b4435b381aab8fd325 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 21:36:48 -0800 Subject: [PATCH] fix tests --- packages/brick_sqlite/lib/brick_sqlite.dart | 1 + .../src/helpers/query_sql_transformer.dart | 80 ++++--- .../lib/src/sqlite_provider_query.dart | 35 +++ .../test/query_sql_transformer_test.dart | 203 +++++++++++++++++- 4 files changed, 278 insertions(+), 41 deletions(-) create mode 100644 packages/brick_sqlite/lib/src/sqlite_provider_query.dart diff --git a/packages/brick_sqlite/lib/brick_sqlite.dart b/packages/brick_sqlite/lib/brick_sqlite.dart index 560f06aa..58d9f749 100644 --- a/packages/brick_sqlite/lib/brick_sqlite.dart +++ b/packages/brick_sqlite/lib/brick_sqlite.dart @@ -5,3 +5,4 @@ export 'package:brick_sqlite/src/runtime_sqlite_column_definition.dart'; export 'package:brick_sqlite/src/sqlite_adapter.dart'; export 'package:brick_sqlite/src/sqlite_model_dictionary.dart'; export 'package:brick_sqlite/src/sqlite_provider.dart'; +export 'package:brick_sqlite/src/sqlite_provider_query.dart'; diff --git a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart index 729ed3ba..02956abb 100644 --- a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart +++ b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart @@ -5,6 +5,8 @@ import 'package:brick_sqlite/src/models/sqlite_model.dart'; import 'package:brick_sqlite/src/runtime_sqlite_column_definition.dart'; import 'package:brick_sqlite/src/sqlite_adapter.dart'; import 'package:brick_sqlite/src/sqlite_model_dictionary.dart'; +import 'package:brick_sqlite/src/sqlite_provider.dart'; +import 'package:brick_sqlite/src/sqlite_provider_query.dart'; import 'package:meta/meta.dart' show protected; /// Create a prepared SQLite statement for eventual execution. Only [statement] and [values] @@ -89,7 +91,7 @@ class QuerySqlTransformer<_Model extends SqliteModel> { _statement.add( AllOtherClausesFragment( - query?.providerArgs ?? {}, + query, fieldsToColumns: adapter.fieldsToSqliteColumns, ).toString(), ); @@ -343,7 +345,7 @@ class AllOtherClausesFragment { final Map fieldsToColumns; /// - final Map providerArgs; + final Query? query; /// Order matters. For example, LIMIT has to follow an ORDER BY but precede an OFFSET. static const _supportedOperators = { @@ -355,48 +357,62 @@ class AllOtherClausesFragment { 'offset': 'OFFSET', }; - /// These operators declare a column to compare against. The fields provided in [providerArgs] - /// will have to be converted to their column name. + /// These operators declare a column to compare against. The fields provided in + /// [Query] or [SqliteProviderQuery] will have to be converted to their column name. /// For example, `orderBy: [OrderBy.asc('createdAt')]` must become `ORDER BY created_at ASC`. static const _operatorsDeclaringFields = {'ORDER BY', 'GROUP BY', 'HAVING'}; /// Query modifiers such as `LIMIT`, `OFFSET`, etc. that require minimal logic. AllOtherClausesFragment( - Map? providerArgs, { + this.query, { required this.fieldsToColumns, - }) : providerArgs = providerArgs ?? {}; + }); @override - String toString() => _supportedOperators.entries.fold>([], (acc, entry) { - final op = entry.value; - var value = providerArgs[entry.key]; - - if (value == null) return acc; - - if (_operatorsDeclaringFields.contains(op)) { - value = value.toString().split(',').fold(value.toString(), - (modValue, innerValueClause) { - final fragment = innerValueClause.trim().split(' '); - if (fragment.isEmpty) return modValue; - - final fieldName = fragment.first; - final columnDefinition = fieldsToColumns[fieldName]; - var columnName = columnDefinition?.columnName; - if (columnName != null && modValue.contains(fieldName)) { - if (columnDefinition!.type == DateTime) { - columnName = 'datetime($columnName)'; - } - return modValue.replaceAll(fieldName, columnName); + String toString() { + final providerQuery = query?.providerQueries[SqliteProvider] as SqliteProviderQuery?; + final argsToSqlStatments = { + ...?query?.providerArgs, + if (providerQuery?.collate != null) 'collate': providerQuery?.collate, + if (query?.limit != null) 'limit': query?.limit, + if (query?.offset != null) 'offset': query?.offset, + if (query?.orderBy.isNotEmpty ?? false) + 'orderBy': query?.orderBy.map((p) => p.toString()).join(', '), + if (providerQuery?.groupBy != null) 'groupBy': providerQuery?.groupBy, + if (providerQuery?.having != null) 'having': providerQuery?.having, + }; + + return _supportedOperators.entries.fold>([], (acc, entry) { + final op = entry.value; + var value = argsToSqlStatments[entry.key]; + + if (value == null) return acc; + + if (_operatorsDeclaringFields.contains(op)) { + value = value.toString().split(',').fold(value.toString(), + (modValue, innerValueClause) { + final fragment = innerValueClause.trim().split(' '); + if (fragment.isEmpty) return modValue; + + final fieldName = fragment.first; + final columnDefinition = fieldsToColumns[fieldName]; + var columnName = columnDefinition?.columnName; + if (columnName != null && modValue.contains(fieldName)) { + if (columnDefinition!.type == DateTime) { + columnName = 'datetime($columnName)'; } + return modValue.replaceAll(fieldName, columnName); + } - return modValue; - }); - } + return modValue; + }); + } - acc.add('$op $value'); + acc.add('$op $value'); - return acc; - }).join(' '); + return acc; + }).join(' '); + } } // Taken directly from the Dart collection package diff --git a/packages/brick_sqlite/lib/src/sqlite_provider_query.dart b/packages/brick_sqlite/lib/src/sqlite_provider_query.dart new file mode 100644 index 00000000..803bd4ca --- /dev/null +++ b/packages/brick_sqlite/lib/src/sqlite_provider_query.dart @@ -0,0 +1,35 @@ +import 'package:brick_core/query.dart'; +import 'package:brick_sqlite/src/sqlite_provider.dart'; + +class SqliteProviderQuery extends ProviderQuery { + final String? collate; + + final String? groupBy; + + final String? having; + + const SqliteProviderQuery({ + this.collate, + this.groupBy, + this.having, + }); + + @override + Map toJson() => { + if (collate != null) 'collate': collate, + if (groupBy != null) 'groupBy': groupBy, + if (having != null) 'having': having, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SqliteProviderQuery && + runtimeType == other.runtimeType && + collate == other.collate && + groupBy == other.groupBy && + having == other.having; + + @override + int get hashCode => collate.hashCode ^ groupBy.hashCode ^ having.hashCode; +} diff --git a/packages/brick_sqlite/test/query_sql_transformer_test.dart b/packages/brick_sqlite/test/query_sql_transformer_test.dart index 2cbf29e8..8206089f 100644 --- a/packages/brick_sqlite/test/query_sql_transformer_test.dart +++ b/packages/brick_sqlite/test/query_sql_transformer_test.dart @@ -1,5 +1,6 @@ import 'package:brick_core/query.dart'; import 'package:brick_sqlite/src/helpers/query_sql_transformer.dart'; +import 'package:brick_sqlite/src/sqlite_provider_query.dart'; import 'package:sqflite_common/sqlite_api.dart'; import 'package:sqflite_common/src/mixin/factory.dart'; import 'package:test/test.dart'; @@ -390,7 +391,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: const Query(limit: 1), + query: const Query(providerArgs: {'limit': 1}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -400,11 +401,64 @@ void main() { test('providerArgs.offset', () async { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1 OFFSET 1'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'limit': 1, 'offset': 1}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + group('providerArgs.orderBy', () { + test('simple', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY id DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'orderBy': 'id DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('discovered columns share similar names', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY last_name DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'orderBy': 'lastName DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + }); + + test('providerArgs.orderBy expands field names to column names', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY many_assoc DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'orderBy': 'manyAssoc DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('providerArgs.orderBy compound values are expanded to column names', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY many_assoc DESC, complex_field_name ASC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, query: const Query( - limit: 1, - offset: 1, + providerArgs: { + 'orderBy': 'manyAssoc DESC, complexFieldName ASC', + }, ), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -413,7 +467,136 @@ void main() { sqliteStatementExpectation(statement); }); - group('providerArgs.orderBy', () { + test('fields convert to column names in providerArgs values', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name ASC GROUP BY complex_field_name HAVING complex_field_name > 1000'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query( + providerArgs: { + 'having': 'complexFieldName > 1000', + 'groupBy': 'complexFieldName', + 'orderBy': 'complexFieldName ASC', + }, + ), + ); + + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('date time is converted', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY datetime(simple_time) DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'orderBy': 'simpleTime DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + // This behavior is not explicitly supported - field names should be used. + // This is considered functionality behavior and is not guaranteed in + // future Brick releases. + // https://github.com/GetDutchie/brick/issues/429 + test('incorrectly cased columns are forwarded as is', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'orderBy': 'complex_field_name DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + // This behavior is not explicitly supported because table names are autogenerated + // and not configurable. This is considered functionality behavior and is not + // guaranteed in future Brick releases. + // https://github.com/GetDutchie/brick/issues/429 + test('ordering by association is forwarded as is', () async { + const statement = + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY other_table.complex_field_name DESC'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(providerArgs: {'orderBy': 'other_table.complex_field_name DESC'}), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + }); + + group('Query', () { + test('#collate', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` COLLATE NOCASE'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(forProviders: [SqliteProviderQuery(collate: 'NOCASE')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#groupBy', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` GROUP BY id'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(forProviders: [SqliteProviderQuery(groupBy: 'id')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#having', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` HAVING id'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(forProviders: [SqliteProviderQuery(having: 'id')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#limit', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(limit: 1), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + test('#offset', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1 OFFSET 1'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(limit: 1, offset: 1), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); + + group('#orderBy', () { test('simple', () async { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY id DESC'; final sqliteQuery = QuerySqlTransformer( @@ -474,10 +657,12 @@ void main() { modelDictionary: dictionary, query: Query( orderBy: [OrderBy.asc('complexFieldName')], - providerArgs: { - 'having': 'complexFieldName > 1000', - 'groupBy': 'complexFieldName', - }, + forProviders: [ + const SqliteProviderQuery( + having: 'complexFieldName > 1000', + groupBy: 'complexFieldName', + ), + ], ), ); @@ -526,7 +711,7 @@ void main() { 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY other_table.complex_field_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: const Query(providerArgs: {'orderBy': 'other_table.complex_field_name DESC'}), + query: Query(orderBy: [OrderBy.desc('other_table.complex_field_name')]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values);