From 7f97f71eea684ed4c0a07fb36207cb59effd101d Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 14:34:53 -0800 Subject: [PATCH 1/5] update analysis of sqlite --- .../src/offline_first_sqlite_builders.dart | 2 +- packages/brick_sqlite/analysis_options.yaml | 325 ++++++++++++++++++ packages/brick_sqlite/lib/db.dart | 1 + .../lib/memory_cache_provider.dart | 22 +- .../lib/src/annotations/sqlite.dart | 7 +- .../src/annotations/sqlite_serializable.dart | 2 + packages/brick_sqlite/lib/src/db/column.dart | 75 ++++ .../brick_sqlite/lib/src/db/migratable.dart | 3 + .../brick_sqlite/lib/src/db/migration.dart | 111 +----- .../db/migration_commands/create_index.dart | 5 + .../db/migration_commands/drop_column.dart | 6 + .../src/db/migration_commands/drop_index.dart | 2 + .../src/db/migration_commands/drop_table.dart | 2 + .../db/migration_commands/insert_column.dart | 21 +- .../insert_foreign_key.dart | 9 +- .../db/migration_commands/insert_table.dart | 4 + .../migration_commands/migration_command.dart | 1 + .../db/migration_commands/rename_column.dart | 6 + .../db/migration_commands/rename_table.dart | 4 + .../lib/src/db/migration_manager.dart | 32 +- .../lib/src/db/schema/schema.dart | 36 +- .../lib/src/db/schema/schema_base.dart | 1 + .../lib/src/db/schema/schema_column.dart | 41 ++- .../lib/src/db/schema/schema_difference.dart | 45 +-- .../lib/src/db/schema/schema_index.dart | 6 + .../lib/src/db/schema/schema_table.dart | 4 + .../lib/src/helpers/alter_column_helper.dart | 53 +-- .../src/helpers/query_sql_transformer.dart | 102 +++--- .../lib/src/models/sqlite_model.dart | 4 +- .../src/runtime_sqlite_column_definition.dart | 2 + .../brick_sqlite/lib/src/sqlite_adapter.dart | 5 + .../lib/src/sqlite_model_dictionary.dart | 1 + .../brick_sqlite/lib/src/sqlite_provider.dart | 67 ++-- packages/brick_sqlite/pubspec.yaml | 12 +- packages/brick_sqlite/test/__mocks__.dart | 5 +- .../test/__mocks__/demo_model_adapter.dart | 154 ++++----- .../__mocks__/demo_model_assoc_adapter.dart | 36 +- packages/brick_sqlite/test/db/__mocks__.dart | 1 + .../brick_sqlite/test/db/column_test.dart | 49 +++ .../test/db/migration_commands_test.dart | 2 +- .../test/db/migration_manager_test.dart | 7 +- .../brick_sqlite/test/db/migration_test.dart | 57 +-- .../test/db/schema_column_test.dart | 25 +- .../test/db/schema_difference_test.dart | 20 +- .../test/db/schema_table_test.dart | 29 +- .../brick_sqlite/test/db/schema_test.dart | 36 +- .../test/memory_cache_provider_test.dart | 15 +- .../test/query_sql_transformer_test.dart | 25 +- .../sqlite_schema_generator.dart | 2 +- 49 files changed, 948 insertions(+), 534 deletions(-) create mode 100644 packages/brick_sqlite/lib/src/db/column.dart create mode 100644 packages/brick_sqlite/test/db/column_test.dart diff --git a/packages/brick_offline_first_build/lib/src/offline_first_sqlite_builders.dart b/packages/brick_offline_first_build/lib/src/offline_first_sqlite_builders.dart index 743eefda..3d90cc04 100644 --- a/packages/brick_offline_first_build/lib/src/offline_first_sqlite_builders.dart +++ b/packages/brick_offline_first_build/lib/src/offline_first_sqlite_builders.dart @@ -16,7 +16,7 @@ class OfflineFirstSchemaGenerator extends SqliteSchemaGenerator { final sqliteChecker = checkerForType(sqliteSerializerType); return SchemaColumn( column.name!, - Migration.fromDartPrimitive(sqliteChecker.asPrimitive), + Column.fromDartPrimitive(sqliteChecker.asPrimitive), nullable: column.nullable, unique: column.unique, ); diff --git a/packages/brick_sqlite/analysis_options.yaml b/packages/brick_sqlite/analysis_options.yaml index f04c6cf0..5008bf6c 100644 --- a/packages/brick_sqlite/analysis_options.yaml +++ b/packages/brick_sqlite/analysis_options.yaml @@ -1 +1,326 @@ include: ../../analysis_options.yaml + +linter: + rules: + # This list is derived from the list of all available lints located at + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + # - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first + # - always_specify_types + - always_use_package_imports + - annotate_overrides + # - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses + # - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + # - avoid_dynamic_calls + - avoid_empty_else + # - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + # - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + # - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + # - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - join_return_with_assignment + # - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + # - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + # - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + # - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + # - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + # - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + # - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + # - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks + +analyzer: + exclude: + - example/ + - "**/example/" + - example_rest + - example_graphql + - "**/*.g.dart" + + errors: + # override custom + always_use_package_imports: error + camel_case_extensions: error + camel_case_types: error + curly_braces_in_flow_control_structures: error + directives_ordering: error + file_names: error + prefer_single_quotes: error + prefer_is_empty: error + prefer_is_not_empty: error + require_trailing_commas: error + sort_pub_dependencies: error + unnecessary_statements: error + + # override flutter_lints + avoid_print: error + avoid_unnecessary_containers: error + avoid_web_libraries_in_flutter: error + no_logic_in_create_state: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + prefer_const_declarations: error + prefer_const_literals_to_create_immutables: error + sized_box_for_whitespace: error + sort_child_properties_last: error + use_build_context_synchronously: error + use_full_hex_values_for_flutter_colors: error + use_key_in_widget_constructors: error + + # override recommended lints + always_require_non_null_named_parameters: error + annotate_overrides: error + avoid_function_literals_in_foreach_calls: error + avoid_init_to_null: error + avoid_null_checks_in_equality_operators: error + avoid_renaming_method_parameters: error + avoid_return_types_on_setters: error + avoid_returning_null_for_void: error + avoid_single_cascade_in_expression_statements: error + await_only_futures: error + constant_identifier_names: error + control_flow_in_finally: error + depend_on_referenced_packages: ignore + empty_constructor_bodies: error + empty_statements: error + exhaustive_cases: error + implementation_imports: ignore + invalid_case_patterns: error + library_names: error + library_prefixes: error + library_private_types_in_public_api: error + matching_super_parameters: error + no_leading_underscores_for_library_prefixes: error + no_leading_underscores_for_local_identifiers: error + no_literal_bool_comparisons: error + null_check_on_nullable_type_parameter: error + null_closures: error + overridden_fields: error + package_names: error + prefer_adjacent_string_concatenation: error + prefer_collection_literals: error + prefer_conditional_assignment: error + prefer_contains: error + prefer_equal_for_default_values: error + prefer_final_fields: error + prefer_for_elements_to_map_fromIterable: error + prefer_function_declarations_over_variables: error + prefer_if_null_operators: error + prefer_initializing_formals: error + prefer_inlined_adds: error + prefer_interpolation_to_compose_strings: error + prefer_is_not_operator: error + prefer_null_aware_operators: error + prefer_spread_collections: error + prefer_void_to_null: error + recursive_getters: error + slash_for_doc_comments: error + type_init_formals: error + type_literal_in_constant_pattern: error + unnecessary_brace_in_string_interps: error + unnecessary_breaks: error + unnecessary_const: error + unnecessary_constructor_name: error + unnecessary_getters_setters: error + unnecessary_late: error + unnecessary_new: error + unnecessary_null_aware_assignments: error + unnecessary_null_in_if_null_operators: error + unnecessary_nullable_for_final_variable_declarations: error + unnecessary_string_escapes: error + unnecessary_string_interpolations: error + unnecessary_this: error + use_function_type_syntax_for_parameters: error + use_rethrow_when_possible: error diff --git a/packages/brick_sqlite/lib/db.dart b/packages/brick_sqlite/lib/db.dart index 972f4393..fc6ff6b5 100644 --- a/packages/brick_sqlite/lib/db.dart +++ b/packages/brick_sqlite/lib/db.dart @@ -1,3 +1,4 @@ +export 'package:brick_sqlite/src/db/column.dart'; export 'package:brick_sqlite/src/db/migratable.dart'; export 'package:brick_sqlite/src/db/migration.dart'; export 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; diff --git a/packages/brick_sqlite/lib/memory_cache_provider.dart b/packages/brick_sqlite/lib/memory_cache_provider.dart index 9ac8f1f1..944bb881 100644 --- a/packages/brick_sqlite/lib/memory_cache_provider.dart +++ b/packages/brick_sqlite/lib/memory_cache_provider.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; /// MemoryCacheProvider does not have a type argument due to a build_runner /// exception: https://github.com/dart-lang/sdk/issues/38309 class MemoryCacheProvider extends Provider { + /// @protected final Logger logger = Logger('MemoryCacheProvider'); @@ -29,7 +30,7 @@ class MemoryCacheProvider extends Provider managedModelTypes.contains(type); /// It is strongly recommended to use this provider with smaller, frequently-accessed - /// and shared [TModel]s. + /// and shared [TProviderModel]s. MemoryCacheProvider([ this.managedModelTypes = const [], ]); @@ -46,7 +47,11 @@ class MemoryCacheProvider extends Provider(instance, {query, repository}) { + bool delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) { if (!manages(TModel)) return false; logger.finest('#delete: $TModel, $instance, $query'); @@ -56,7 +61,10 @@ class MemoryCacheProvider extends Provider? get({query, repository}) { + List? get({ + Query? query, + ModelRepository? repository, + }) { if (!manages(TModel)) return null; managedObjects[TModel] ??= {}; @@ -98,11 +106,15 @@ class MemoryCacheProvider extends Provider(instance, {query, repository}) { + TModel? upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) { if (!manages(TModel)) return null; logger.finest('#upsert: $TModel, $instance, $query'); hydrate([instance]); - return managedObjects[TModel]![instance.primaryKey] as TModel; + return managedObjects[TModel]![instance.primaryKey]! as TModel; } } diff --git a/packages/brick_sqlite/lib/src/annotations/sqlite.dart b/packages/brick_sqlite/lib/src/annotations/sqlite.dart index 3b7c9251..3c5179f7 100644 --- a/packages/brick_sqlite/lib/src/annotations/sqlite.dart +++ b/packages/brick_sqlite/lib/src/annotations/sqlite.dart @@ -1,7 +1,8 @@ import 'package:brick_core/field_serializable.dart'; -import 'package:brick_sqlite/src/db/migration.dart' show Column; +import 'package:brick_sqlite/src/annotations/sqlite_serializable.dart'; +import 'package:brick_sqlite/src/db/column.dart'; -export 'package:brick_sqlite/src/db/migration.dart' show Column; +export 'package:brick_sqlite/src/db/column.dart'; /// An annotation used to specify how a field is serialized. /// Heavily inspired by [JsonKey](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_key.dart) @@ -62,7 +63,7 @@ class Sqlite implements FieldSerializable { @override final bool nullable; - /// When true, deletion of the referenced record by [foreignKeyColumn] on the [foreignTableName] + /// When true, deletion of the referenced record by `foreignKeyColumn` on the `foreignTableName` /// this record. For example, if the foreign table is "departments" and the local table /// is "employees," whenever that department is deleted, "employee" /// will be deleted. Defaults `false`. diff --git a/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart b/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart index 58f24e71..88da54ab 100644 --- a/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart +++ b/packages/brick_sqlite/lib/src/annotations/sqlite_serializable.dart @@ -1,3 +1,5 @@ +import 'package:brick_sqlite/src/annotations/sqlite.dart'; + /// An annotation used to specify a class to generate code for. /// /// Creates a serialize/deserialize function and a Schema output diff --git a/packages/brick_sqlite/lib/src/db/column.dart b/packages/brick_sqlite/lib/src/db/column.dart new file mode 100644 index 00000000..4dbfe118 --- /dev/null +++ b/packages/brick_sqlite/lib/src/db/column.dart @@ -0,0 +1,75 @@ +// Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/migration.dart) + +import 'dart:core' as core; +import 'dart:core'; + +/// SQLite data types. +/// +/// While SQLite only supports 5 datatypes, it will still cast these +/// into an [intelligent affinity](https://www.sqlite.org/datatype3.html). +enum Column { + /// No data type + undefined._('', dynamic), + + /// + bigint._('BIGINT', core.num), + + /// + blob._('BLOB', List), + + /// + boolean._('BOOLEAN', bool), + + /// + date._('DATE', DateTime), + + /// + datetime._('DATETIME', DateTime), + + /// + // ignore: constant_identifier_names + Double._('DOUBLE', double), + + /// + integer._('INTEGER', int), + + /// + float._('FLOAT', core.num), + + /// + num._('DOUBLE', core.num), + + /// + text._('TEXT', String), + + /// + varchar._('VARCHAR', String); + + /// The equivalent Dart primitive + final Type dartType; + + /// SQLite equivalent type + final core.String definition; + + const Column._(this.definition, this.dartType); + + /// Convert native Dart to `Column` + factory Column.fromDartPrimitive(Type type) { + switch (type) { + case bool: + return Column.boolean; + case DateTime: + return Column.datetime; + case double: + return Column.Double; + case int: + return Column.integer; + case core.num: + return Column.num; + case String: + return Column.varchar; + default: + return throw ArgumentError('$type not associated with a Column'); + } + } +} diff --git a/packages/brick_sqlite/lib/src/db/migratable.dart b/packages/brick_sqlite/lib/src/db/migratable.dart index af67c87f..b987b2b7 100644 --- a/packages/brick_sqlite/lib/src/db/migratable.dart +++ b/packages/brick_sqlite/lib/src/db/migratable.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Annotation required by the generator for AOT discoverability. Decorates classes @@ -18,6 +19,8 @@ class Migratable { /// the version should still match `RegExp("^\d+$")`. final String version; + /// Annotation required by the generator for AOT discoverability. Decorates classes + /// that `extends Migration`. const Migratable({ required this.down, required this.up, diff --git a/packages/brick_sqlite/lib/src/db/migration.dart b/packages/brick_sqlite/lib/src/db/migration.dart index b4bd9db9..974ebb43 100644 --- a/packages/brick_sqlite/lib/src/db/migration.dart +++ b/packages/brick_sqlite/lib/src/db/migration.dart @@ -1,126 +1,35 @@ // Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/migration.dart) - import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; +import 'package:brick_sqlite/src/db/schema/schema.dart'; -/// SQLite data types. -/// -/// While SQLite only supports 5 datatypes, it will still cast these -/// into an [intelligent affinity](https://www.sqlite.org/datatype3.html). -enum Column { - undefined, - bigint, - blob, - boolean, - date, - datetime, - // ignore: constant_identifier_names - Double, - integer, - float, - - /// DOUBLE column type is used - num, - text, - varchar -} - +/// A collection of [MigrationCommand]s to update the [Schema]. abstract class Migration { /// Order to run; should be unique and sequential with other [Migration]s final int version; + /// Desired changes to the [Schema]. final List up; + + /// Reverts [up] final List down; + /// A collection of [MigrationCommand]s to update the [Schema]. const Migration({ required this.version, required this.up, required this.down, }); + /// Alias of [upStatement] String get statement => upStatement; + /// Generate SQL statements for all commands String get upStatement => '${up.map((c) => c.statement).join(';\n')};'; + /// Revert of [upStatement] String get downStatement => '${down.map((c) => c.statement).join(';\n')};'; - /// Convert `Column` to SQLite data types - static String ofDefinition(Column definition) { - switch (definition) { - case Column.bigint: - return 'BIGINT'; - case Column.boolean: - return 'BOOLEAN'; - case Column.blob: - return 'BLOB'; - case Column.date: - return 'DATE'; - case Column.datetime: - return 'DATETIME'; - case Column.Double: - case Column.num: - return 'DOUBLE'; - case Column.integer: - return 'INTEGER'; - case Column.float: - return 'FLOAT'; - case Column.text: - return 'TEXT'; - case Column.varchar: - return 'VARCHAR'; - default: - return throw ArgumentError('$definition not found in Column'); - } - } - - /// Convert native Dart to `Column` - static Column fromDartPrimitive(Type type) { - switch (type) { - case bool: - return Column.boolean; - case DateTime: - return Column.datetime; - case double: - return Column.Double; - case int: - return Column.integer; - case num: - return Column.num; - case String: - return Column.varchar; - default: - return throw ArgumentError('$type not associated with a Column'); - } - } - - /// Convert `Column` to native Dart - static Type toDartPrimitive(Column definition) { - switch (definition) { - case Column.bigint: - return num; - case Column.boolean: - return bool; - case Column.blob: - return List; - case Column.date: - return DateTime; - case Column.datetime: - return DateTime; - case Column.Double: - return double; - case Column.integer: - return int; - case Column.float: - case Column.num: - return num; - case Column.text: - return String; - case Column.varchar: - return String; - default: - return throw ArgumentError('$definition not found in Column'); - } - } - + /// SQL command to produce the migration static String generate(List commands, int version) { final upCommands = commands.map((m) => m.forGenerator); final downCommands = commands.map((m) => m.down?.forGenerator).toList().whereType(); diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart b/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart index b5493001..45ed1d60 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/create_index.dart @@ -3,10 +3,13 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Create an index on a table if it doesn't already exists class CreateIndex extends MigrationCommand { + /// final List columns; + /// final String onTable; + /// final bool unique; /// As a migration, this may fail if existing data is in conflict with the index. @@ -18,6 +21,7 @@ class CreateIndex extends MigrationCommand { this.unique = false, }); + /// String get name => generateName(columns, onTable); @override @@ -37,6 +41,7 @@ class CreateIndex extends MigrationCommand { @override MigrationCommand get down => DropIndex(name); + /// Combines columns and table name to create an index name static String generateName(List columns, String onTable) { final columnNames = columns.join('_'); return ['index', onTable, 'on', columnNames].join('_'); diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart b/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart index ee468770..05d0408b 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/drop_column.dart @@ -4,9 +4,15 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// columns prefixed by `_should_drop` and generate a statement that includes the schema of /// the full table to be `ALTER`ed. class DropColumn extends MigrationCommand { + /// final String name; + + /// final String onTable; + /// SQLite doesn't have a catch-all drop column command. On migrate, the provider can search for + /// columns prefixed by `_should_drop` and generate a statement that includes the schema of + /// the full table to be `ALTER`ed. const DropColumn( this.name, { required this.onTable, diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart b/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart index dcc03de0..c8200391 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/drop_index.dart @@ -2,8 +2,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Drop index from DB if it exists class DropIndex extends MigrationCommand { + /// final String name; + /// Drop index from DB if it exists const DropIndex(this.name); @override diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart b/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart index 7a2348e9..40b8903a 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/drop_table.dart @@ -3,8 +3,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Drop table from DB if it exists class DropTable extends MigrationCommand { + /// final String name; + /// Drop table from DB if it exists const DropTable(this.name); @override diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart b/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart index 61500537..b90855e5 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/insert_column.dart @@ -1,11 +1,16 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Creates a new SQLite column in a table class InsertColumn extends MigrationCommand { + /// final String name; + + /// Column type final Column definitionType; + + /// final String onTable; /// Column can be `NULL`. Defaults `true`. @@ -20,6 +25,7 @@ class InsertColumn extends MigrationCommand { /// Column has `UNIQUE` constraint. Defaults `false`. final bool unique; + /// Creates a new SQLite column in a table const InsertColumn( this.name, this.definitionType, { @@ -39,16 +45,19 @@ class InsertColumn extends MigrationCommand { } String get _nullStatement => nullable ? 'NULL' : 'NOT NULL'; + String? get _autoincrementStatement { if (!autoincrement) return null; return 'AUTOINCREMENT'; } - String get definition => Migration.ofDefinition(definitionType); + /// + String get definition => definitionType.definition; + String get _addons { - final list = [_autoincrementStatement, _nullStatement, _defaultStatement]; - list.removeWhere((s) => s == null); + final list = [_autoincrementStatement, _nullStatement, _defaultStatement] + ..removeWhere((s) => s == null); return list.join(' '); } @@ -85,12 +94,10 @@ class InsertColumn extends MigrationCommand { @override MigrationCommand get down => DropColumn(name, onTable: onTable); + /// Defaults static const InsertColumn defaults = InsertColumn( 'PLACEHOLDER', Column.varchar, onTable: 'PLACEHOLDER', - autoincrement: false, - nullable: true, - unique: false, ); } diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart b/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart index e6028a17..e5bb4ec3 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/insert_foreign_key.dart @@ -4,7 +4,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Create a foreign key column to reference another table class InsertForeignKey extends MigrationCommand { + /// Table where the foreign key is defined final String localTableName; + + /// Table referenced by the foreign key final String foreignTableName; /// Defaults to lowercase `${foreignTableName}_brick_id` @@ -20,6 +23,7 @@ class InsertForeignKey extends MigrationCommand { /// usually `NULL` unless otherwise declared. Defaults `false`. final bool onDeleteSetDefault; + /// Create a foreign key column to reference another table const InsertForeignKey( this.localTableName, this.foreignTableName, { @@ -76,9 +80,8 @@ class InsertForeignKey extends MigrationCommand { /// The downside of this pattern is the inevitable data duplication for such many-to-many /// relationships and the inability to query relationships without declaring them on /// parent/child models. - static String joinsTableName(String columnName, {required String localTableName}) { - return ['_brick', localTableName, columnName].join('_'); - } + static String joinsTableName(String columnName, {required String localTableName}) => + ['_brick', localTableName, columnName].join('_'); /// In the rare case of a many-to-many association of the same model, the columns must be prefixed. /// For example, `final List friends` on class `Friend`. diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart b/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart index d432a72c..1000f98b 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/insert_table.dart @@ -3,8 +3,10 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Insert table if it doesn't already exist class InsertTable extends MigrationCommand { + /// final String name; + /// Insert table if it doesn't already exist const InsertTable(this.name); @override @@ -20,6 +22,8 @@ class InsertTable extends MigrationCommand { /// Automatically aliased to [rowid](https://www.sqlite.org/lang_createtable.html#rowid). // ignore: constant_identifier_names static const PRIMARY_KEY_COLUMN = '_brick_id'; + + /// Dart field name of the primary key, pulled from the [PRIMARY_KEY_COLUMN] // ignore: constant_identifier_names static const PRIMARY_KEY_FIELD = 'primaryKey'; } diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart b/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart index 33d1218c..f7281241 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/migration_command.dart @@ -9,6 +9,7 @@ abstract class MigrationCommand { /// Outputs the opposite command to be used in a generator MigrationCommand? get down => null; + /// Extendible interface for SQLite migrations const MigrationCommand(); /// Alias for [statement] diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart b/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart index 405e1b06..e940b808 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/rename_column.dart @@ -2,10 +2,16 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Renames an existing SQLite column in a table class RenameColumn extends MigrationCommand { + /// final String oldName; + + /// final String newName; + + /// final String onTable; + /// Renames an existing SQLite column in a table const RenameColumn( this.oldName, this.newName, { diff --git a/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart b/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart index 6b09667e..3e474710 100644 --- a/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart +++ b/packages/brick_sqlite/lib/src/db/migration_commands/rename_table.dart @@ -2,9 +2,13 @@ import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Renames an existing SQLite table class RenameTable extends MigrationCommand { + /// final String oldName; + + /// final String newName; + /// Renames an existing SQLite table const RenameTable( this.oldName, this.newName, diff --git a/packages/brick_sqlite/lib/src/db/migration_manager.dart b/packages/brick_sqlite/lib/src/db/migration_manager.dart index e4f26f41..21e7408f 100644 --- a/packages/brick_sqlite/lib/src/db/migration_manager.dart +++ b/packages/brick_sqlite/lib/src/db/migration_manager.dart @@ -1,48 +1,39 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/db.dart'; import 'package:meta/meta.dart'; /// Holds all migrations and outputs statements for SQLite to consume class MigrationManager { + /// @protected final Set migrations; + /// Holds all migrations and outputs statements for SQLite to consume const MigrationManager(this.migrations); /// Identifies the latest migrations, especially those not yet added to the [Schema] /// The delta between [Schema]'s version and [MigrationManager]'s are unprocessed migrations - int get version { - return latestMigrationVersion(migrations); - } + int get version => latestMigrationVersion(migrations); /// Key/value migrations based on their version - Map get migrationByVersion { - return {for (var m in migrations) m.version: m}; - } + Map get migrationByVersion => {for (final m in migrations) m.version: m}; /// Migrations after a version /// /// [versionNumber] defaults to [version] List migrationsSince([int? versionNumber]) { final number = versionNumber ?? version; - final validMigrations = migrations.where((m) => m.version > number).toList(); - validMigrations.sort((a, b) => a.version.compareTo(b.version)); - return validMigrations; + return migrations.where((m) => m.version > number).toList() + ..sort((a, b) => a.version.compareTo(b.version)); } /// Migrations before and including a version /// /// [versionNumber] defaults to [version] - Map migrationsUntil([int? versionNumber]) { - return migrationByVersion - ..removeWhere((version, _) { - return version > (versionNumber ?? version); - }); - } + Map migrationsUntil([int? versionNumber]) => + migrationByVersion..removeWhere((version, _) => version > (versionNumber ?? version)); /// Migration at a version - Migration? migrationAt(int versionNumber) { - return migrationByVersion[versionNumber]; - } + Migration? migrationAt(int versionNumber) => migrationByVersion[versionNumber]; /// Sort migrations by their version number in ascending order /// and return the latest [Migration] version or `0` if [allMigrations] is empty @@ -51,8 +42,7 @@ class MigrationManager { return 0; } - final versions = allMigrations.map((m) => m.version).toList(); - versions.sort(); + final versions = allMigrations.map((m) => m.version).toList()..sort(); return versions.last; } } diff --git a/packages/brick_sqlite/lib/src/db/schema/schema.dart b/packages/brick_sqlite/lib/src/db/schema/schema.dart index 7eb13418..6fb85e98 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema.dart @@ -1,5 +1,6 @@ // Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/schema_builder.dart) // Unfortunately, some key differences such as inability to use mirrors and the sqlite vs postgres capabilities make DIY a more palatable option than retrofitting +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; @@ -17,32 +18,37 @@ import 'package:brick_sqlite/src/db/schema/schema_index.dart'; import 'package:brick_sqlite/src/db/schema/schema_table.dart'; import 'package:meta/meta.dart' show visibleForTesting; +/// A definition of all the tables and columns in the SQLite database class Schema { /// The last version successfully migrated to SQLite. /// This should be before or equal to [MigrationManager]'s `#version`. /// if [MigrationManager] is used. final int version; + /// final Set tables; /// Version used to produce this scheme final int generatorVersion; + /// A definition of all the tables and columns in the SQLite database Schema(this.version, {required this.tables, this.generatorVersion = GENERATOR_VERSION}); + /// Generator used to produce this schema; not intended for public use // ignore: constant_identifier_names static const int GENERATOR_VERSION = 1; + /// @visibleForTesting static List expandMigrations(Set migrations) { - final sorted = migrations.toList(); - sorted.sort((a, b) { - if (a.version == b.version) { - return 0; - } + final sorted = migrations.toList() + ..sort((a, b) { + if (a.version == b.version) { + return 0; + } - return a.version > b.version ? 1 : -1; - }); + return a.version > b.version ? 1 : -1; + }); return sorted.map((m) => m.up).expand((c) => c).toList(); } @@ -50,7 +56,7 @@ class Schema { /// Create a schema from a set of migrations. If [version] is not provided, /// the highest migration version will be used factory Schema.fromMigrations(Set migrations, [int? version]) { - assert((version == null) || (version > -1)); + assert((version == null) || (version > -1), 'version must be greater than -1'); version = version ?? MigrationManager.latestMigrationVersion(migrations); final commands = expandMigrations(migrations); final tables = commands.fold({}, _commandToSchema); @@ -61,14 +67,12 @@ class Schema { ); } - /// A sub-function of [fromMigrations], convert a migration command into a `SchemaObject`. + /// A sub-function of [Schema.fromMigrations], convert a migration command into a `SchemaObject`. static Set _commandToSchema(Set tables, MigrationCommand command) { - SchemaTable findTable(String tableName) { - return tables.firstWhere( - (s) => s.name == tableName, - orElse: () => throw StateError('Table $tableName must be inserted first'), - ); - } + SchemaTable findTable(String tableName) => tables.firstWhere( + (s) => s.name == tableName, + orElse: () => throw StateError('Table $tableName must be inserted first'), + ); if (command is InsertTable) { tables.add(SchemaTable(command.name)); @@ -86,6 +90,7 @@ class Schema { } else if (command is RenameTable) { final table = findTable(command.oldName); tables.add(SchemaTable(command.newName, columns: table.columns..toSet())); + // ignore: cascade_invocations tables.remove(table); } else if (command is DropTable) { final table = findTable(command.name); @@ -98,7 +103,6 @@ class Schema { command.definitionType, autoincrement: command.autoincrement, defaultValue: command.defaultValue, - isPrimaryKey: false, nullable: command.nullable, unique: command.unique, ), diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_base.dart b/packages/brick_sqlite/lib/src/db/schema/schema_base.dart index 94c74fe5..7a40687e 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_base.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_base.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; /// Generates code for [Migration] from [BaseSchemaObject]ss diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_column.dart b/packages/brick_sqlite/lib/src/db/schema/schema_column.dart index 28c3bb0e..e5515bcd 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_column.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_column.dart @@ -1,6 +1,6 @@ // Heavily, heavily inspired by [Aqueduct](https://github.com/stablekernel/aqueduct/blob/master/aqueduct/lib/src/db/schema/schema_builder.dart) // Unfortunately, some key differences such as inability to use mirrors and the sqlite vs postgres capabilities make DIY a more palatable option than retrofitting -import 'package:brick_sqlite/src/db/migration.dart' show Column; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; @@ -10,20 +10,44 @@ import 'package:brick_sqlite/src/db/schema/schema_base.dart'; /// Describes a column object managed by SQLite /// This should not exist outside of a SchemaTable class SchemaColumn extends BaseSchemaObject { + /// String name; + + /// If this column has an autoincrement value final bool autoincrement; + + /// final Column columnType; + + /// final dynamic defaultValue; + + /// final bool nullable; + + /// final bool isPrimaryKey; + + /// final bool isForeignKey; + + /// final String? foreignTableName; + + /// Remove row when a referenced foreign key is deleted final bool onDeleteCascade; + + /// Update column's value to default when a referenced foreign key is deleted final bool onDeleteSetDefault; + + /// If this column's value is unique within the table final bool unique; + /// String? tableName; + /// Describes a column object managed by SQLite + /// This should not exist outside of a SchemaTable SchemaColumn( this.name, this.columnType, { @@ -40,7 +64,10 @@ class SchemaColumn extends BaseSchemaObject { nullable = nullable ?? InsertColumn.defaults.nullable, unique = unique ?? InsertColumn.defaults.unique, assert(!isPrimaryKey || columnType == Column.integer, 'Primary key must be an integer'), - assert(!isForeignKey || (foreignTableName != null)); + assert( + !isForeignKey || (foreignTableName != null), + 'Foreign key must have a foreign table name', + ); @override String get forGenerator { @@ -63,10 +90,12 @@ class SchemaColumn extends BaseSchemaObject { } if (isForeignKey) { - parts.add('isForeignKey: $isForeignKey'); - parts.add("foreignTableName: '$foreignTableName'"); - parts.add('onDeleteCascade: $onDeleteCascade'); - parts.add('onDeleteSetDefault: $onDeleteSetDefault'); + parts.addAll([ + 'isForeignKey: $isForeignKey', + "foreignTableName: '$foreignTableName'", + 'onDeleteCascade: $onDeleteCascade', + 'onDeleteSetDefault: $onDeleteSetDefault', + ]); } if (unique != InsertColumn.defaults.unique) { diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart b/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart index 1ab9bed7..3df080b8 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_difference.dart @@ -1,31 +1,40 @@ -import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; -import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart'; -import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; -import 'package:brick_sqlite/src/db/schema/schema.dart'; -import 'package:brick_sqlite/src/db/schema/schema_column.dart'; -import 'package:brick_sqlite/src/db/schema/schema_index.dart'; -import 'package:brick_sqlite/src/db/schema/schema_table.dart'; +import 'package:brick_sqlite/db.dart'; import 'package:collection/collection.dart'; /// Compares two schemas to produce migrations that conver the difference class SchemaDifference { + /// final Schema oldSchema; + + /// final Schema newSchema; - SchemaDifference(this.oldSchema, this.newSchema) : assert(oldSchema.version < newSchema.version); + /// Compares two schemas to produce migrations that conver the difference + SchemaDifference(this.oldSchema, this.newSchema) + : assert( + oldSchema.version < newSchema.version, + 'Old schema is a newer version than the new schema', + ); + /// Set get droppedTables => oldSchema.tables.difference(newSchema.tables); + /// Set get insertedTables => newSchema.tables.difference(oldSchema.tables); + /// Set get droppedIndices => _compareIndices(oldSchema, newSchema); + /// Set get createdIndices => _compareIndices(newSchema, oldSchema); + /// Set get droppedColumns => _compareColumns(oldSchema, newSchema); + /// Set get insertedColumns => _compareColumns(newSchema, oldSchema); + /// If there is a significant difference between both schemas bool get hasDifference => droppedTables.isNotEmpty || insertedTables.isNotEmpty || @@ -36,27 +45,22 @@ class SchemaDifference { /// Generates migration commands from the schemas' differences List toMigrationCommands() { - final removedTables = droppedTables.map((item) { - return item.toCommand(shouldDrop: true); - }).cast(); + final removedTables = + droppedTables.map((item) => item.toCommand(shouldDrop: true)).cast(); - // TODO detect if dropped column is a foreign key joins association AND WRITE TEST + // TODOdetect if dropped column is a foreign key joins association AND WRITE TEST // Only drop column if the table isn't being dropped too final removedColumns = droppedColumns - .where((item) { - return !removedTables.any((command) => command.name == item.tableName); - }) + .where((item) => !removedTables.any((command) => command.name == item.tableName)) .map((c) => c.toCommand(shouldDrop: true)) .cast(); final addedColumns = insertedColumns.where((c) => !c.isPrimaryKey).toSet(); final added = [insertedTables, addedColumns] - .map((generatedSet) { - return generatedSet.map((item) { - return item.toCommand(); - }); - }) + .map( + (generatedSet) => generatedSet.map((item) => item.toCommand()), + ) .expand((s) => s) .cast(); @@ -82,6 +86,7 @@ class SchemaDifference { final fromColumns = {}..addAll(fromTable.columns); // Primary keys are added on [InsertTable] + // ignore: cascade_invocations fromColumns.removeWhere((c) => c.isPrimaryKey); toColumns.removeWhere((c) => c.isPrimaryKey); diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_index.dart b/packages/brick_sqlite/lib/src/db/schema/schema_index.dart index 933762a6..c970f037 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_index.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_index.dart @@ -2,15 +2,21 @@ import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; import 'package:brick_sqlite/src/db/schema/schema_base.dart'; +/// A definition for the schema of an index class SchemaIndex extends BaseSchemaObject { + /// String? name; + /// final List columns; + /// String? tableName; + /// final bool unique; + /// A definition for the schema of an index SchemaIndex({ required this.columns, this.tableName, diff --git a/packages/brick_sqlite/lib/src/db/schema/schema_table.dart b/packages/brick_sqlite/lib/src/db/schema/schema_table.dart index a8e9edab..46e9c50b 100644 --- a/packages/brick_sqlite/lib/src/db/schema/schema_table.dart +++ b/packages/brick_sqlite/lib/src/db/schema/schema_table.dart @@ -10,12 +10,16 @@ import 'package:brick_sqlite/src/db/schema/schema_index.dart'; /// Describes a table object managed by SQLite class SchemaTable extends BaseSchemaObject { + /// final String name; + /// Set columns; + /// Set indices; + /// Describes a table object managed by SQLite SchemaTable( this.name, { Set? columns, diff --git a/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart b/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart index ba4ab242..ea927732 100644 --- a/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart +++ b/packages/brick_sqlite/lib/src/helpers/alter_column_helper.dart @@ -11,15 +11,21 @@ class AlterColumnHelper { /// The command to restructure the table final MigrationCommand command; + /// bool get isDrop => command is DropColumn; + + /// bool get isRename => command is RenameColumn; + + /// bool get isUniqueInsert => command is InsertColumn && (command as InsertColumn).unique; /// Declares if this command requires extra SQLite work to be migrated bool get requiresSchema => isDrop || isRename || isUniqueInsert; + /// String get tableName { - assert(requiresSchema); + assert(requiresSchema, 'Command does not require schema'); if (isDrop) { return (command as DropColumn).onTable; @@ -32,12 +38,14 @@ class AlterColumnHelper { return (command as InsertColumn).onTable; } - AlterColumnHelper(this.command); + /// Workaround for SQLite commands that require altering the table instead of the column. + /// + /// Supports [DropColumn], [RenameColumn], [InsertColumn] + const AlterColumnHelper(this.command); /// Get info about existing columns - Future>> tableInfo(Database db) async { - return await db.rawQuery('PRAGMA table_info("$tableName");'); - } + Future>> tableInfo(Database db) async => + await db.rawQuery('PRAGMA table_info("$tableName");'); /// Create new table with updated column data List> _newColumns(List> columns) { @@ -73,29 +81,28 @@ class AlterColumnHelper { } /// Given new columns, create the SQLite statement - String _newColumnsExpression(List> columns) { - return columns.map((Map column) { - final definition = [column['name'] as String, column['type'] as String]; + String _newColumnsExpression(List> columns) => + columns.map((Map column) { + final definition = [column['name'] as String, column['type'] as String]; - if (column['notnull'] == 1) { - definition.add('NOT NULL'); - } + if (column['notnull'] == 1) { + definition.add('NOT NULL'); + } - if (column['dflt_value'] != null) { - definition.add('DEFAULT ${column['dflt_value']}'); - } + if (column['dflt_value'] != null) { + definition.add('DEFAULT ${column['dflt_value']}'); + } - if (column['pk'] == 1) { - definition.add('PRIMARY KEY'); - } + if (column['pk'] == 1) { + definition.add('PRIMARY KEY'); + } - if (column['unique'] == true) { - definition.add('UNIQUE'); - } + if (column['unique'] == true) { + definition.add('UNIQUE'); + } - return definition.join(' '); - }).join(', '); - } + return definition.join(' '); + }).join(', '); /// Perform the necessary SQLite operation Future execute(Database db) async { 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 38bcfa39..729ed3ba 100644 --- a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart +++ b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart @@ -1,4 +1,4 @@ -import 'package:brick_core/core.dart' show Query, WhereCondition, Compare, WherePhrase; +import 'package:brick_core/core.dart' show Compare, Query, WhereCondition, WherePhrase; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; import 'package:brick_sqlite/src/models/sqlite_model.dart'; @@ -16,8 +16,12 @@ import 'package:meta/meta.dart' show protected; /// final results = await (await db).rawQuery(sqliteQuery.statement, sqliteQuery.values); /// ``` class QuerySqlTransformer<_Model extends SqliteModel> { + /// The [SqliteAdapter] of [_Model] final SqliteAdapter adapter; + + /// All other [SqliteAdapter]s and [SqliteModel]s known to the provider final SqliteModelDictionary modelDictionary; + final List _statement = []; final List _where = []; final Set _innerJoins = {}; @@ -40,13 +44,14 @@ class QuerySqlTransformer<_Model extends SqliteModel> { /// Prepared; includes preceeding `WHERE` String get whereClause { if (_where.isNotEmpty) { - final cleanedClause = _cleanWhereClause(_where.join('')); + final cleanedClause = _cleanWhereClause(_where.join()); return 'WHERE $cleanedClause'; } return ''; } + /// All `INNER JOIN` statements String get innerJoins => _innerJoins.join(' '); /// [selectStatement] will output [statement] as a `SELECT FROM`. When false, the [statement] @@ -94,13 +99,11 @@ class QuerySqlTransformer<_Model extends SqliteModel> { /// prefixed with their `required` operator before being added to the full WHERE clause. /// This is a bad hack to remove leading operators (otherwise it's invalid SQL) /// and should be refactored after experimental use in the wild. - String _cleanWhereClause(String dirtyClause) { - return dirtyClause - .replaceFirst(RegExp('^ (AND|OR)'), '') - .replaceAll(RegExp(r' \( (AND|OR)'), ' (') - .replaceAll(RegExp(r'\(\s+'), '(') - .trim(); - } + String _cleanWhereClause(String dirtyClause) => dirtyClause + .replaceFirst(RegExp('^ (AND|OR)'), '') + .replaceAll(RegExp(r' \( (AND|OR)'), ' (') + .replaceAll(RegExp(r'\(\s+'), '(') + .trim(); /// Recursively step through a `Where` or `WherePhrase` to ouput a condition for `WHERE`. String _expandCondition(WhereCondition condition, [SqliteAdapter? passedAdapter]) { @@ -108,9 +111,10 @@ class QuerySqlTransformer<_Model extends SqliteModel> { // Begin a separate where phrase if (condition is WherePhrase) { - final phrase = condition.conditions.fold('', (acc, phraseCondition) { - return acc + _expandCondition(phraseCondition, passedAdapter); - }); + final phrase = condition.conditions.fold( + '', + (acc, phraseCondition) => acc + _expandCondition(phraseCondition, passedAdapter), + ); if (phrase.isEmpty) return ''; final matcher = condition.isRequired ? 'AND' : 'OR'; @@ -175,20 +179,25 @@ class QuerySqlTransformer<_Model extends SqliteModel> { } } -/// Inner joins +/// Inner joins; not for public use class AssociationFragment { + /// final String foreignTableName; + /// final RuntimeSqliteColumnDefinition definition; + /// final String localTableName; + /// Inner joins; not for public use AssociationFragment({ required this.definition, required this.foreignTableName, required this.localTableName, }); + /// All statements to create the `INNER JOIN` command List toJoinFragment() { const primaryKeyColumn = InsertTable.PRIMARY_KEY_COLUMN; final oneToOneAssociation = !definition.iterable; @@ -213,12 +222,16 @@ class AssociationFragment { /// Column and iterable comparison class WhereColumnFragment { + /// final String column; + /// final WhereCondition condition; + /// String get matcher => condition.isRequired ? 'AND' : 'OR'; + /// The sign used to compare statements generated by [compareSign] String get sign { if (condition.value == null) { if (condition.compare == Compare.exact) return 'IS'; @@ -228,11 +241,13 @@ class WhereColumnFragment { return compareSign(condition.compare); } + /// Values used for the `WHERE` clause final List values = []; /// Computed once after initialization by [generate] late final String? _statement; + /// Column and iterable comparison WhereColumnFragment( this.condition, this.column, @@ -240,6 +255,7 @@ class WhereColumnFragment { _statement = generate(); } + /// SQLite statement from all `WHERE` conditions and values @protected String generate() { if (condition.value is Iterable) { @@ -259,6 +275,7 @@ class WhereColumnFragment { return ' $matcher $column $sign ?'; } + /// Convert special cases to SQLite statements @protected dynamic sqlifiedValue(dynamic value, Compare compare) { if (compare == Compare.contains || compare == Compare.doesNotContain) { @@ -273,6 +290,7 @@ class WhereColumnFragment { @override String toString() => _statement ?? ''; + /// Convert [Compare] values to SQLite-usable operators static String compareSign(Compare compare) { switch (compare) { case Compare.exact: @@ -321,7 +339,10 @@ class WhereColumnFragment { /// Query modifiers such as `LIMIT`, `OFFSET`, etc. that require minimal logic. class AllOtherClausesFragment { + /// final Map fieldsToColumns; + + /// final Map providerArgs; /// Order matters. For example, LIMIT has to follow an ORDER BY but precede an OFFSET. @@ -339,44 +360,43 @@ class AllOtherClausesFragment { /// 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, { required this.fieldsToColumns, }) : providerArgs = providerArgs ?? {}; @override - String toString() { - return _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)'; + 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); } - 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/models/sqlite_model.dart b/packages/brick_sqlite/lib/src/models/sqlite_model.dart index b0e54da4..1cc5a8d3 100644 --- a/packages/brick_sqlite/lib/src/models/sqlite_model.dart +++ b/packages/brick_sqlite/lib/src/models/sqlite_model.dart @@ -1,11 +1,13 @@ import 'package:brick_core/core.dart'; +import 'package:brick_sqlite/src/sqlite_provider.dart'; +/// The default value of the `primaryKey` field. // ignore: constant_identifier_names const int? NEW_RECORD_ID = null; /// Models accessible to the [SqliteProvider]. /// -/// Why isn't this in the SQLite package? It's required by [OfflineFirstModel]. +/// Why isn't this in the SQLite package? It's required by `OfflineFirstModel`. abstract class SqliteModel implements Model { /// DO NOT modify this in the end implementation code. The Repository will update it accordingly. /// It is strongly recommended that this field only be used by Brick's internal queries and not diff --git a/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart b/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart index a7ce2610..7bc19574 100644 --- a/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart +++ b/packages/brick_sqlite/lib/src/runtime_sqlite_column_definition.dart @@ -16,6 +16,8 @@ class RuntimeSqliteColumnDefinition { /// In other words, the runtime type. final Type type; + /// Used to define types in [SqliteAdapter#fieldsToSqliteColumns]. The build runner package + /// extracts types and associations that would've been otherwise inaccessible at runtime. const RuntimeSqliteColumnDefinition({ this.association = false, required this.columnName, diff --git a/packages/brick_sqlite/lib/src/sqlite_adapter.dart b/packages/brick_sqlite/lib/src/sqlite_adapter.dart index b4b610ad..138b0aa3 100644 --- a/packages/brick_sqlite/lib/src/sqlite_adapter.dart +++ b/packages/brick_sqlite/lib/src/sqlite_adapter.dart @@ -1,4 +1,6 @@ import 'package:brick_core/core.dart'; +import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart'; 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_provider.dart'; @@ -29,11 +31,14 @@ abstract class SqliteAdapter implements Adapter { ModelRepository? repository, }) async {} + /// Future fromSqlite( Map input, { required SqliteProvider provider, ModelRepository? repository, }); + + /// Future> toSqlite( TModel input, { required SqliteProvider provider, diff --git a/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart b/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart index 5ae174d3..b6bdd778 100644 --- a/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart +++ b/packages/brick_sqlite/lib/src/sqlite_model_dictionary.dart @@ -4,5 +4,6 @@ import 'package:brick_sqlite/src/sqlite_adapter.dart'; /// Associates app models with their [SqliteAdapter] class SqliteModelDictionary extends ModelDictionary> { + /// Associates app models with their [SqliteAdapter] const SqliteModelDictionary(super.adapterFor); } diff --git a/packages/brick_sqlite/lib/src/sqlite_provider.dart b/packages/brick_sqlite/lib/src/sqlite_provider.dart index d8eb2f6b..165c820d 100644 --- a/packages/brick_sqlite/lib/src/sqlite_provider.dart +++ b/packages/brick_sqlite/lib/src/sqlite_provider.dart @@ -14,7 +14,7 @@ import 'package:sqflite_common/sqlite_api.dart'; import 'package:sqflite_common/utils/utils.dart' as sqlite_utils; import 'package:synchronized/synchronized.dart'; -/// Retrieves from a Sqlite database +/// Retrieves from a SQLite database class SqliteProvider implements Provider { /// Access the [SQLite](https://github.com/tekartik/sqflite/tree/master/sqflite_common_ffi), /// instance agnostically across platforms. @@ -42,6 +42,7 @@ class SqliteProvider implements Provider implements Provider delete(instance, {query, repository}) async { + Future delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final db = await getDb(); final existingPrimaryKey = await adapter.primaryKeyByUniqueColumns(instance, db); @@ -71,7 +76,7 @@ class SqliteProvider implements Provider exists({ Query? query, @@ -91,9 +96,11 @@ class SqliteProvider implements Provider + 'SELECT ${match.group(1)}.${InsertTable.PRIMARY_KEY_COLUMN} FROM ${match.group(1)}', + ); } final countQuery = await (await getDb()).rawQuery(statement, sqlQuery.values); @@ -120,8 +127,8 @@ class SqliteProvider implements Provider> get({ - query, - repository, + Query? query, + ModelRepository? repository, }) async { final sqlQuery = QuerySqlTransformer( modelDictionary: modelDictionary, @@ -141,6 +148,7 @@ class SqliteProvider implements Provider lastMigrationVersion() async { final db = await getDb(); @@ -160,7 +168,7 @@ class SqliteProvider implements Provider implements Provider implements Provider (await getDb()).rawQuery(sql, arguments)); if (results.isEmpty || results.first.isEmpty) { // otherwise an empty sql result will generate a blank model @@ -229,19 +235,16 @@ class SqliteProvider implements Provider rawExecute(String sql, [List? arguments]) async { - return await (await getDb()).execute(sql, arguments); - } + Future rawExecute(String sql, [List? arguments]) async => + await (await getDb()).execute(sql, arguments); /// Insert with a raw SQL statement. **Advanced use only**. - Future rawInsert(String sql, [List? arguments]) async { - return await (await getDb()).rawInsert(sql, arguments); - } + Future rawInsert(String sql, [List? arguments]) async => + await (await getDb()).rawInsert(sql, arguments); /// Query with a raw SQL statement. **Advanced use only**. - Future>> rawQuery(String sql, [List? arguments]) async { - return await (await getDb()).rawQuery(sql, arguments); - } + Future>> rawQuery(String sql, [List? arguments]) async => + await (await getDb()).rawQuery(sql, arguments); /// Reset the DB by deleting and recreating it. /// @@ -265,14 +268,16 @@ class SqliteProvider implements Provider transaction(Future Function(Transaction transaction) callback) async { final db = await getDb(); - return await _lock.synchronized(() async { - return await db.transaction(callback); - }); + return await _lock.synchronized(() async => await db.transaction(callback)); } /// Insert record into SQLite. Returns the primary key of the record inserted @override - Future upsert(instance, {query, repository}) async { + Future upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final db = await getDb(); @@ -280,8 +285,8 @@ class SqliteProvider implements Provider((txn) async { + final id = await _lock.synchronized( + () async => await db.transaction((txn) async { final existingPrimaryKey = await adapter.primaryKeyByUniqueColumns(instance, txn); if (instance.isNewRecord && existingPrimaryKey == null) { @@ -299,8 +304,8 @@ class SqliteProvider implements Provider=2.18.0 <4.0.0" dependencies: - brick_core: ^1.1.1 + brick_core: ">=2.0.0 <3.0.0" collection: ">=1.0.0 <2.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" sqflite_common: ">=2.0.0 <3.0.0" - synchronized: ^3.0.0 + synchronized: ">=3.0.0 <4.0.0" dev_dependencies: - test: ^1.16.5 - mockito: ^5.0.0 - sqflite_common_ffi: ^2.0.0 - lints: ^2.0.1 + lints: + mockito: + sqflite_common_ffi: + test: diff --git a/packages/brick_sqlite/test/__mocks__.dart b/packages/brick_sqlite/test/__mocks__.dart index 5e820ccd..43f8001b 100644 --- a/packages/brick_sqlite/test/__mocks__.dart +++ b/packages/brick_sqlite/test/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; @@ -22,21 +23,17 @@ const _demoModelMigrationCommands = [ 'DemoModel', foreignKeyColumn: 'l_DemoModel_brick_id', onDeleteCascade: true, - onDeleteSetDefault: false, ), InsertForeignKey( '_brick_DemoModel_many_assoc', 'DemoModelAssoc', foreignKeyColumn: 'f_DemoModelAssoc_brick_id', onDeleteCascade: true, - onDeleteSetDefault: false, ), InsertForeignKey( 'DemoModel', 'DemoModelAssoc', foreignKeyColumn: 'assoc_DemoModelAssoc_brick_id', - onDeleteCascade: false, - onDeleteSetDefault: false, ), InsertColumn('complex_field_name', Column.varchar, onTable: 'DemoModel'), InsertColumn('last_name', Column.varchar, onTable: 'DemoModel'), diff --git a/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart b/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart index 779875fb..719a75dd 100644 --- a/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart +++ b/packages/brick_sqlite/test/__mocks__/demo_model_adapter.dart @@ -1,4 +1,5 @@ import 'package:brick_core/core.dart' show Query; +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:sqflite_common/sqlite_api.dart' show DatabaseExecutor; @@ -8,66 +9,64 @@ Future _$DemoModelFromSqlite( Map data, { SqliteProvider? provider, repository, -}) async { - return DemoModel( - name: data['full_name'] == null ? null : data['full_name'] as String, - assoc: data['assoc_DemoModelAssoc_brick_id'] == null - ? null - : (data['assoc_DemoModelAssoc_brick_id'] > -1 - ? (await repository?.getAssociation( - Query.where( - 'primaryKey', - data['assoc_DemoModelAssoc_brick_id'] as int, - limit1: true, - ), - )) - ?.first - : null), - complexFieldName: - data['complex_field_name'] == null ? null : data['complex_field_name'] as String, - lastName: data['last_name'] == null ? null : data['last_name'] as String, - manyAssoc: (await provider?.rawQuery( - 'SELECT DISTINCT `f_DemoModelAssoc_brick_id` FROM `_brick_DemoModel_many_assoc` WHERE l_DemoModel_brick_id = ?', - [data['_brick_id'] as int], - ).then((results) { - final ids = results.map((r) => r['f_DemoModelAssoc_brick_id']); - return Future.wait( - ids.map( - (primaryKey) => repository - ?.getAssociation( - Query.where('primaryKey', primaryKey, limit1: true), - ) - ?.then((r) => (r?.isEmpty ?? true) ? null : r.first), - ), - ); - })) - ?.toList(), - simpleBool: data['simple_bool'] == null ? null : data['simple_bool'] == 1, - simpleTime: data['simple_time'] == null - ? null - : data['simple_time'] == null - ? null - : DateTime.tryParse(data['simple_time'] as String), - )..primaryKey = data['_brick_id'] as int; -} +}) async => + DemoModel( + name: data['full_name'] == null ? null : data['full_name'] as String, + assoc: data['assoc_DemoModelAssoc_brick_id'] == null + ? null + : (data['assoc_DemoModelAssoc_brick_id'] > -1 + ? (await repository?.getAssociation( + Query.where( + 'primaryKey', + data['assoc_DemoModelAssoc_brick_id'] as int, + limit1: true, + ), + )) + ?.first + : null), + complexFieldName: + data['complex_field_name'] == null ? null : data['complex_field_name'] as String, + lastName: data['last_name'] == null ? null : data['last_name'] as String, + manyAssoc: (await provider?.rawQuery( + 'SELECT DISTINCT `f_DemoModelAssoc_brick_id` FROM `_brick_DemoModel_many_assoc` WHERE l_DemoModel_brick_id = ?', + [data['_brick_id'] as int], + ).then((results) { + final ids = results.map((r) => r['f_DemoModelAssoc_brick_id']); + return Future.wait( + ids.map( + (primaryKey) => repository + ?.getAssociation( + Query.where('primaryKey', primaryKey, limit1: true), + ) + ?.then((r) => (r?.isEmpty ?? true) ? null : r.first), + ), + ); + })) + ?.toList(), + simpleBool: data['simple_bool'] == null ? null : data['simple_bool'] == 1, + simpleTime: data['simple_time'] == null + ? null + : data['simple_time'] == null + ? null + : DateTime.tryParse(data['simple_time'] as String), + )..primaryKey = data['_brick_id'] as int; Future> _$DemoModelToSqlite( DemoModel instance, { required SqliteProvider provider, repository, -}) async { - return { - 'assoc_DemoModelAssoc_brick_id': instance.assoc?.primaryKey ?? - (instance.assoc != null - ? await provider.upsert(instance.assoc!, repository: repository) - : null), - 'complex_field_name': instance.complexFieldName, - 'last_name': instance.lastName, - 'full_name': instance.name, - 'simple_bool': instance.simpleBool == null ? null : (instance.simpleBool! ? 1 : 0), - 'simple_time': instance.simpleTime?.toIso8601String(), - }; -} +}) async => + { + 'assoc_DemoModelAssoc_brick_id': instance.assoc?.primaryKey ?? + (instance.assoc != null + ? await provider.upsert(instance.assoc!, repository: repository) + : null), + 'complex_field_name': instance.complexFieldName, + 'last_name': instance.lastName, + 'full_name': instance.name, + 'simple_bool': instance.simpleBool == null ? null : (instance.simpleBool! ? 1 : 0), + 'simple_time': instance.simpleTime?.toIso8601String(), + }; /// Construct a [DemoModel] class DemoModelAdapter extends SqliteAdapter { @@ -76,39 +75,28 @@ class DemoModelAdapter extends SqliteAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'id': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'id', - iterable: false, type: int, ), 'someField': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'some_field', - iterable: false, type: bool, ), 'assoc': const RuntimeSqliteColumnDefinition( association: true, columnName: 'assoc_DemoModelAssoc_brick_id', - iterable: false, type: DemoModelAssoc, ), 'complexFieldName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'complex_field_name', - iterable: false, type: String, ), 'lastName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'last_name', - iterable: false, type: String, ), 'manyAssoc': const RuntimeSqliteColumnDefinition( @@ -118,21 +106,15 @@ class DemoModelAdapter extends SqliteAdapter { type: DemoModelAssoc, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'full_name', - iterable: false, type: String, ), 'simpleBool': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'simple_bool', - iterable: false, type: bool, ), - 'simpleTime': RuntimeSqliteColumnDefinition( - association: false, + 'simpleTime': const RuntimeSqliteColumnDefinition( columnName: 'simple_time', - iterable: false, type: DateTime, ), }; @@ -144,7 +126,11 @@ class DemoModelAdapter extends SqliteAdapter { @override final String tableName = 'DemoModel'; @override - Future afterSave(instance, {required provider, repository}) async { + Future afterSave( + DemoModel instance, { + required SqliteProvider provider, + ModelRepository? repository, + }) async { if (instance.primaryKey != null) { final oldColumns = await provider.rawQuery( 'SELECT `f_DemoModelAssoc_brick_id` FROM `_brick_DemoModel_many_assoc` WHERE `l_DemoModel_brick_id` = ?', @@ -155,12 +141,12 @@ class DemoModelAdapter extends SqliteAdapter { final idsToDelete = oldIds.where((id) => !newIds.contains(id)); await Future.wait( - idsToDelete.map((id) async { - return await provider.rawExecute( + idsToDelete.map( + (id) async => await provider.rawExecute( 'DELETE FROM `_brick_DemoModel_many_assoc` WHERE `l_DemoModel_brick_id` = ? AND `f_DemoModelAssoc_brick_id` = ?', [instance.primaryKey, id], - ); - }), + ), + ), ); await Future.wait( @@ -178,9 +164,17 @@ class DemoModelAdapter extends SqliteAdapter { } @override - Future fromSqlite(Map input, {required provider, repository}) async => + Future fromSqlite( + Map input, { + required SqliteProvider provider, + ModelRepository? repository, + }) async => await _$DemoModelFromSqlite(input, provider: provider, repository: repository); @override - Future> toSqlite(DemoModel input, {required provider, repository}) async => + Future> toSqlite( + DemoModel input, { + required SqliteProvider provider, + ModelRepository? repository, + }) async => await _$DemoModelToSqlite(input, provider: provider, repository: repository); } diff --git a/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart b/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart index 47368f98..aba53fd7 100644 --- a/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart +++ b/packages/brick_sqlite/test/__mocks__/demo_model_assoc_adapter.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; // ignore: unused_import, unused_shown_name import 'package:brick_sqlite/db.dart'; @@ -9,18 +10,16 @@ Future _$DemoModelAssocFromSqlite( Map data, { SqliteProvider? provider, repository, -}) async { - return DemoModelAssoc(name: data['full_name'] == null ? null : data['full_name'] as String) - ..primaryKey = data['_brick_id'] as int; -} +}) async => + DemoModelAssoc(name: data['full_name'] == null ? null : data['full_name'] as String) + ..primaryKey = data['_brick_id'] as int; Future> _$DemoModelAssocToSqlite( DemoModelAssoc instance, { SqliteProvider? provider, repository, -}) async { - return {'full_name': instance.name}; -} +}) async => + {'full_name': instance.name}; /// Construct a [DemoModelAssoc] class DemoModelAssocAdapter extends SqliteAdapter { @@ -29,39 +28,28 @@ class DemoModelAssocAdapter extends SqliteAdapter { @override final Map fieldsToSqliteColumns = { 'primaryKey': const RuntimeSqliteColumnDefinition( - association: false, columnName: '_brick_id', - iterable: false, type: int, ), 'id': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'id', - iterable: false, type: int, ), 'someField': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'some_field', - iterable: false, type: bool, ), 'assoc': const RuntimeSqliteColumnDefinition( association: true, columnName: 'assoc_DemoModelAssoc_brick_id', - iterable: false, type: DemoModelAssoc, ), 'complexFieldName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'complex_field_name', - iterable: false, type: String, ), 'lastName': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'last_name', - iterable: false, type: String, ), 'manyAssoc': const RuntimeSqliteColumnDefinition( @@ -71,15 +59,11 @@ class DemoModelAssocAdapter extends SqliteAdapter { type: DemoModelAssoc, ), 'name': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'full_name', - iterable: false, type: String, ), 'simpleBool': const RuntimeSqliteColumnDefinition( - association: false, columnName: 'simple_bool', - iterable: false, type: bool, ), }; @@ -97,15 +81,15 @@ class DemoModelAssocAdapter extends SqliteAdapter { @override Future fromSqlite( Map input, { - required provider, - repository, + required SqliteProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocFromSqlite(input, provider: provider, repository: repository); @override Future> toSqlite( DemoModelAssoc input, { - required provider, - repository, + required SqliteProvider provider, + ModelRepository? repository, }) async => await _$DemoModelAssocToSqlite(input, provider: provider, repository: repository); } diff --git a/packages/brick_sqlite/test/db/__mocks__.dart b/packages/brick_sqlite/test/db/__mocks__.dart index 659287f6..fc2ccc0e 100644 --- a/packages/brick_sqlite/test/db/__mocks__.dart +++ b/packages/brick_sqlite/test/db/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_index.dart'; diff --git a/packages/brick_sqlite/test/db/column_test.dart b/packages/brick_sqlite/test/db/column_test.dart new file mode 100644 index 00000000..d905c2b6 --- /dev/null +++ b/packages/brick_sqlite/test/db/column_test.dart @@ -0,0 +1,49 @@ +import 'package:brick_sqlite/src/db/column.dart'; +import 'package:test/test.dart'; + +void main() { + group('Column', () { + test('#definition', () { + expect(Column.bigint.definition, 'BIGINT'); + expect(Column.blob.definition, 'BLOB'); + expect(Column.boolean.definition, 'BOOLEAN'); + expect(Column.date.definition, 'DATE'); + expect(Column.datetime.definition, 'DATETIME'); + expect(Column.Double.definition, 'DOUBLE'); + expect(Column.integer.definition, 'INTEGER'); + expect(Column.float.definition, 'FLOAT'); + expect(Column.num.definition, 'DOUBLE'); + expect(Column.text.definition, 'TEXT'); + expect(Column.varchar.definition, 'VARCHAR'); + expect(Column.undefined.definition, ''); + }); + + test('.fromDartPrimitive', () { + expect(Column.fromDartPrimitive(bool), Column.boolean); + expect(Column.fromDartPrimitive(DateTime), Column.datetime); + expect(Column.fromDartPrimitive(double), Column.Double); + expect(Column.fromDartPrimitive(int), Column.integer); + expect(Column.fromDartPrimitive(num), Column.num); + expect(Column.fromDartPrimitive(String), Column.varchar); + expect( + () => Column.fromDartPrimitive(dynamic), + throwsA(const TypeMatcher()), + ); + }); + + test('#dartType', () { + expect(Column.bigint.dartType, num); + expect(Column.blob.dartType, List); + expect(Column.boolean.dartType, bool); + expect(Column.date.dartType, DateTime); + expect(Column.datetime.dartType, DateTime); + expect(Column.Double.dartType, double); + expect(Column.integer.dartType, int); + expect(Column.float.dartType, num); + expect(Column.num.dartType, num); + expect(Column.text.dartType, String); + expect(Column.varchar.dartType, String); + expect(Column.undefined.dartType, dynamic); + }); + }); +} diff --git a/packages/brick_sqlite/test/db/migration_commands_test.dart b/packages/brick_sqlite/test/db/migration_commands_test.dart index 54f5d753..72b42946 100644 --- a/packages/brick_sqlite/test/db/migration_commands_test.dart +++ b/packages/brick_sqlite/test/db/migration_commands_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_index.dart'; diff --git a/packages/brick_sqlite/test/db/migration_manager_test.dart b/packages/brick_sqlite/test/db/migration_manager_test.dart index 6c4b0d0c..5cda808e 100644 --- a/packages/brick_sqlite/test/db/migration_manager_test.dart +++ b/packages/brick_sqlite/test/db/migration_manager_test.dart @@ -21,7 +21,7 @@ void main() { const m2 = Migration2(); const m3 = Migration3(); final manager = MigrationManager({m1, m2, m3}); - final emptyManager = MigrationManager({}); + const emptyManager = MigrationManager({}); test('#migrationsSince', () { expect(manager.migrationsSince(1), hasLength(2)); @@ -49,13 +49,14 @@ void main() { expect(emptyManager.version, 0); - final otherManager = MigrationManager({Migration2(), Migration1()}); + final otherManager = MigrationManager({const Migration2(), const Migration1()}); // Should sort migrations inserted out of order expect(otherManager.version, 2); }); test('.latestMigrationVersion', () { - final version = MigrationManager.latestMigrationVersion([Migration2(), Migration1()]); + final version = + MigrationManager.latestMigrationVersion([const Migration2(), const Migration1()]); expect(version, 2); }); diff --git a/packages/brick_sqlite/test/db/migration_test.dart b/packages/brick_sqlite/test/db/migration_test.dart index e73bd8a8..fd28fef3 100644 --- a/packages/brick_sqlite/test/db/migration_test.dart +++ b/packages/brick_sqlite/test/db/migration_test.dart @@ -27,55 +27,6 @@ class Migration20 extends Migration { void main() { group('Migration', () { - test('.ofDefinition', () { - expect(Migration.ofDefinition(Column.bigint), 'BIGINT'); - expect(Migration.ofDefinition(Column.blob), 'BLOB'); - expect(Migration.ofDefinition(Column.boolean), 'BOOLEAN'); - expect(Migration.ofDefinition(Column.date), 'DATE'); - expect(Migration.ofDefinition(Column.datetime), 'DATETIME'); - expect(Migration.ofDefinition(Column.Double), 'DOUBLE'); - expect(Migration.ofDefinition(Column.integer), 'INTEGER'); - expect(Migration.ofDefinition(Column.float), 'FLOAT'); - expect(Migration.ofDefinition(Column.num), 'DOUBLE'); - expect(Migration.ofDefinition(Column.text), 'TEXT'); - expect(Migration.ofDefinition(Column.varchar), 'VARCHAR'); - expect( - () => Migration.ofDefinition(Column.undefined), - throwsA(const TypeMatcher()), - ); - }); - - test('.fromDartPrimitive', () { - expect(Migration.fromDartPrimitive(bool), Column.boolean); - expect(Migration.fromDartPrimitive(DateTime), Column.datetime); - expect(Migration.fromDartPrimitive(double), Column.Double); - expect(Migration.fromDartPrimitive(int), Column.integer); - expect(Migration.fromDartPrimitive(num), Column.num); - expect(Migration.fromDartPrimitive(String), Column.varchar); - expect( - () => Migration.fromDartPrimitive(dynamic), - throwsA(const TypeMatcher()), - ); - }); - - test('.toDartPrimitive', () { - expect(Migration.toDartPrimitive(Column.bigint), num); - expect(Migration.toDartPrimitive(Column.blob), List); - expect(Migration.toDartPrimitive(Column.boolean), bool); - expect(Migration.toDartPrimitive(Column.date), DateTime); - expect(Migration.toDartPrimitive(Column.datetime), DateTime); - expect(Migration.toDartPrimitive(Column.Double), double); - expect(Migration.toDartPrimitive(Column.integer), int); - expect(Migration.toDartPrimitive(Column.float), num); - expect(Migration.toDartPrimitive(Column.num), num); - expect(Migration.toDartPrimitive(Column.text), String); - expect(Migration.toDartPrimitive(Column.varchar), String); - expect( - () => Migration.toDartPrimitive(Column.undefined), - throwsA(const TypeMatcher()), - ); - }); - group('.generate', () { test('one command', () { final output = Migration.generate([const InsertTable('demo')], 1); @@ -121,8 +72,8 @@ class Migration1 extends Migration { test('multiple commands', () { final commands = [ - InsertTable('demo'), - RenameColumn('first_name', 'last_name', onTable: 'people'), + const InsertTable('demo'), + const RenameColumn('first_name', 'last_name', onTable: 'people'), ]; final output = Migration.generate(commands, 15); @@ -170,8 +121,8 @@ class Migration15 extends Migration { test('null drop commands are not reported', () { final commands = [ - DropColumn('first_name', onTable: 'people'), - DropColumn('last_name', onTable: 'people'), + const DropColumn('first_name', onTable: 'people'), + const DropColumn('last_name', onTable: 'people'), ]; final output = Migration.generate(commands, 15); diff --git a/packages/brick_sqlite/test/db/schema_column_test.dart b/packages/brick_sqlite/test/db/schema_column_test.dart index 06c662ea..062f4fb5 100644 --- a/packages/brick_sqlite/test/db/schema_column_test.dart +++ b/packages/brick_sqlite/test/db/schema_column_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; @@ -10,7 +10,7 @@ void main() { test('isPrimaryKey must be int', () { expect( () => SchemaColumn(InsertTable.PRIMARY_KEY_COLUMN, Column.varchar, isPrimaryKey: true), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -69,8 +69,7 @@ void main() { group('#toCommand', () { test('simple', () { - final column = SchemaColumn('first_name', Column.varchar); - column.tableName = 'demo'; + final column = SchemaColumn('first_name', Column.varchar)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('first_name', Column.varchar, onTable: 'demo'), @@ -79,8 +78,8 @@ void main() { test('primary key', () { final column = - SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true); - column.tableName = 'demo'; + SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true) + ..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('_brick_id', Column.integer, onTable: 'demo', autoincrement: true), @@ -88,8 +87,7 @@ void main() { }); test('defaultValue', () { - final column = SchemaColumn('amount', Column.integer, defaultValue: 0); - column.tableName = 'demo'; + final column = SchemaColumn('amount', Column.integer, defaultValue: 0)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('amount', Column.integer, onTable: 'demo', defaultValue: 0), @@ -97,8 +95,8 @@ void main() { }); test('nullable', () { - final column = SchemaColumn('last_name', Column.varchar, nullable: false); - column.tableName = 'demo'; + final column = SchemaColumn('last_name', Column.varchar, nullable: false) + ..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('last_name', Column.varchar, onTable: 'demo', nullable: false), @@ -107,8 +105,8 @@ void main() { test('association', () { final column = - SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat'); - column.tableName = 'demo'; + SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat') + ..tableName = 'demo'; expect( column.toCommand(), const InsertForeignKey('demo', 'hat', foreignKeyColumn: 'Hat_id'), @@ -116,8 +114,7 @@ void main() { }); test('columnType', () { - final column = SchemaColumn('image', Column.blob); - column.tableName = 'demo'; + final column = SchemaColumn('image', Column.blob)..tableName = 'demo'; expect(column.toCommand(), const InsertColumn('image', Column.blob, onTable: 'demo')); }); }); diff --git a/packages/brick_sqlite/test/db/schema_difference_test.dart b/packages/brick_sqlite/test/db/schema_difference_test.dart index b0c7e998..a0794a32 100644 --- a/packages/brick_sqlite/test/db/schema_difference_test.dart +++ b/packages/brick_sqlite/test/db/schema_difference_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/create_index.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart'; @@ -238,7 +238,7 @@ void main() { expect( diff.toMigrationCommands(), [ - InsertTable('demo'), + const InsertTable('demo'), InsertColumn(column.name, Column.varchar, onTable: column.tableName!), ], ); @@ -254,7 +254,7 @@ void main() { expect( diff.toMigrationCommands(), [ - InsertTable('demo'), + const InsertTable('demo'), InsertColumn(column.name, Column.varchar, onTable: column.tableName!), ], ); @@ -276,7 +276,7 @@ void main() { final newSchema = Schema(1, tables: {}); final diff = SchemaDifference(oldSchema, newSchema); - expect(diff.toMigrationCommands(), [DropTable('demo')]); + expect(diff.toMigrationCommands(), [const DropTable('demo')]); expect(diff.droppedTables, hasLength(1)); expect(diff.insertedTables, isEmpty); }); @@ -320,22 +320,20 @@ void main() { final diff = SchemaDifference(oldSchema, newSchema); expect(diff.toMigrationCommands(), [ - InsertTable('_brick_People_friend'), - InsertForeignKey( + const InsertTable('_brick_People_friend'), + const InsertForeignKey( '_brick_People_friend', 'People', foreignKeyColumn: 'l_People_brick_id', - onDeleteCascade: false, onDeleteSetDefault: true, ), - InsertForeignKey( + const InsertForeignKey( '_brick_People_friend', 'Friend', foreignKeyColumn: 'f_Friend_brick_id', - onDeleteCascade: false, onDeleteSetDefault: true, ), - CreateIndex( + const CreateIndex( columns: ['l_People_brick_id', 'f_Friend_brick_id'], onTable: '_brick_People_friend', unique: true, @@ -368,7 +366,7 @@ void main() { final old = Schema(2, tables: {}); final fresh = Schema(1, tables: {}); - expect(() => SchemaDifference(old, fresh), throwsA(TypeMatcher())); + expect(() => SchemaDifference(old, fresh), throwsA(const TypeMatcher())); }); }); } diff --git a/packages/brick_sqlite/test/db/schema_table_test.dart b/packages/brick_sqlite/test/db/schema_table_test.dart index 67ee252e..679c667c 100644 --- a/packages/brick_sqlite/test/db/schema_table_test.dart +++ b/packages/brick_sqlite/test/db/schema_table_test.dart @@ -1,4 +1,4 @@ -import 'package:brick_sqlite/src/db/migration.dart'; +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart'; @@ -38,7 +38,7 @@ void main() { final table = SchemaTable('users', columns: {}); test('shouldDrop:false', () { - expect(table.toCommand(shouldDrop: false), const InsertTable('users')); + expect(table.toCommand(), const InsertTable('users')); }); test('shouldDrop:true', () { @@ -87,7 +87,7 @@ void main() { test('isPrimaryKey must be int', () { expect( () => SchemaColumn(InsertTable.PRIMARY_KEY_COLUMN, Column.varchar, isPrimaryKey: true), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -139,33 +139,30 @@ void main() { }); test('#toCommand', () { - var column = SchemaColumn('first_name', Column.varchar); - column.tableName = 'demo'; + var column = SchemaColumn('first_name', Column.varchar)..tableName = 'demo'; expect(column.toCommand(), const InsertColumn('first_name', Column.varchar, onTable: 'demo')); - column = SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true); - column.tableName = 'demo'; + column = SchemaColumn('_brick_id', Column.integer, autoincrement: true, isPrimaryKey: true) + ..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('_brick_id', Column.integer, onTable: 'demo', autoincrement: true), ); - column = SchemaColumn('amount', Column.integer, defaultValue: 0); - column.tableName = 'demo'; + column = SchemaColumn('amount', Column.integer, defaultValue: 0)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('amount', Column.integer, onTable: 'demo', defaultValue: 0), ); - column = SchemaColumn('last_name', Column.varchar, nullable: false); - column.tableName = 'demo'; + column = SchemaColumn('last_name', Column.varchar, nullable: false)..tableName = 'demo'; expect( column.toCommand(), const InsertColumn('last_name', Column.varchar, onTable: 'demo', nullable: false), ); - column = SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat'); - column.tableName = 'demo'; + column = SchemaColumn('Hat_id', Column.integer, isForeignKey: true, foreignTableName: 'hat') + ..tableName = 'demo'; expect(column.toCommand(), const InsertForeignKey('demo', 'hat', foreignKeyColumn: 'Hat_id')); column = SchemaColumn( @@ -174,8 +171,7 @@ void main() { isForeignKey: true, foreignTableName: 'hat', onDeleteCascade: true, - ); - column.tableName = 'demo'; + )..tableName = 'demo'; expect( column.toCommand(), const InsertForeignKey('demo', 'hat', foreignKeyColumn: 'Hat_id', onDeleteCascade: true), @@ -187,8 +183,7 @@ void main() { isForeignKey: true, foreignTableName: 'hat', onDeleteSetDefault: true, - ); - column.tableName = 'demo'; + )..tableName = 'demo'; expect( column.toCommand(), const InsertForeignKey( diff --git a/packages/brick_sqlite/test/db/schema_test.dart b/packages/brick_sqlite/test/db/schema_test.dart index 42211fe7..f1458982 100644 --- a/packages/brick_sqlite/test/db/schema_test.dart +++ b/packages/brick_sqlite/test/db/schema_test.dart @@ -1,3 +1,4 @@ +import 'package:brick_sqlite/src/db/column.dart'; import 'package:brick_sqlite/src/db/migration.dart'; import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart'; import 'package:brick_sqlite/src/db/migration_commands/rename_column.dart'; @@ -50,8 +51,8 @@ void main() { group('RenameTable', () { test('without a prior, relevant insert migration', () { expect( - () => Schema.fromMigrations({Migration0None(), renameTable}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), renameTable}), + throwsA(const TypeMatcher()), ); }); @@ -83,8 +84,8 @@ void main() { group('DropTable', () { test('without a prior, relevant insert migration', () { expect( - () => Schema.fromMigrations({Migration0None(), dropTable}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), dropTable}), + throwsA(const TypeMatcher()), ); }); @@ -103,8 +104,8 @@ void main() { group('InsertColumn', () { test('without a prior, relevant InsertTable migration', () { expect( - () => Schema.fromMigrations({Migration0None(), insertColumn}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), insertColumn}), + throwsA(const TypeMatcher()), ); }); @@ -137,15 +138,15 @@ void main() { group('RenameColumn', () { test('without a prior, relevant InsertTable migration', () { expect( - () => Schema.fromMigrations({Migration0None(), renameColumn}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), renameColumn}), + throwsA(const TypeMatcher()), ); }); test('without a prior, relevant InsertColumn migration', () { expect( () => Schema.fromMigrations({insertTable, renameColumn}), - throwsA(TypeMatcher()), + throwsA(const TypeMatcher()), ); }); @@ -178,8 +179,8 @@ void main() { group('InsertForeignKey', () { test('without a prior, relevant InsertTable migration', () { expect( - () => Schema.fromMigrations({Migration0None(), insertForeignKey}), - throwsA(TypeMatcher()), + () => Schema.fromMigrations({const Migration0None(), insertForeignKey}), + throwsA(const TypeMatcher()), ); }); @@ -301,7 +302,7 @@ void main() { }, ); - final newSchema = Schema.fromMigrations({insertTable, Migration2()}); + final newSchema = Schema.fromMigrations({insertTable, const Migration2()}); expect(newSchema.tables, schema.tables); expect(newSchema.version, schema.version); }); @@ -314,20 +315,23 @@ void main() { }); test("version uses the migrations' largest version if not provided", () { - expect(Schema.fromMigrations({Migration2(), Migration1()}).version, 2); + expect(Schema.fromMigrations({const Migration2(), const Migration1()}).version, 2); }); }); test('.expandMigrations', () { - final migrations = {MigrationInsertTable(), MigrationRenameColumn()}; + final migrations = {const MigrationInsertTable(), const MigrationRenameColumn()}; final commands = Schema.expandMigrations(migrations); // Maintains sort order - expect(commands, [InsertTable('demo'), RenameColumn('name', 'first_name', onTable: 'demo')]); + expect( + commands, + [const InsertTable('demo'), const RenameColumn('name', 'first_name', onTable: 'demo')], + ); }); test('#forGenerator', () { - final schema = Schema.fromMigrations({MigrationInsertTable(), Migration2()}); + final schema = Schema.fromMigrations({const MigrationInsertTable(), const Migration2()}); expect(schema.forGenerator, ''' Schema( diff --git a/packages/brick_sqlite/test/memory_cache_provider_test.dart b/packages/brick_sqlite/test/memory_cache_provider_test.dart index 595a5bf0..c04a7c95 100644 --- a/packages/brick_sqlite/test/memory_cache_provider_test.dart +++ b/packages/brick_sqlite/test/memory_cache_provider_test.dart @@ -36,8 +36,7 @@ void main() { }); test('#delete', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); expect(provider.managedObjects, isNotEmpty); expect(provider.managedObjects[Person], isNotEmpty); @@ -53,8 +52,7 @@ void main() { }); test('.id queries', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); final results = provider.get( query: Query( @@ -67,8 +65,7 @@ void main() { }); test('unlimited queries', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); final results = provider.get(); @@ -79,8 +76,7 @@ void main() { test('#hydrate', () { expect(provider.managedObjects, isEmpty); - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); expect(provider.managedObjects, isNotEmpty); expect(provider.managedObjects[Person], isNotNull); @@ -95,8 +91,7 @@ void main() { }); test('#reset', () { - final instance = Person(); - instance.primaryKey = 1; + final instance = Person()..primaryKey = 1; provider.hydrate([instance]); expect(provider.managedObjects[Person], isNotEmpty); provider.reset(); diff --git a/packages/brick_sqlite/test/query_sql_transformer_test.dart b/packages/brick_sqlite/test/query_sql_transformer_test.dart index 3f2a48ce..2cbf29e8 100644 --- a/packages/brick_sqlite/test/query_sql_transformer_test.dart +++ b/packages/brick_sqlite/test/query_sql_transformer_test.dart @@ -21,9 +21,8 @@ class _FakeMethodCall { this.rawFactory = false, }); - factory _FakeMethodCall.fromFactory(String method, dynamic arguments) { - return _FakeMethodCall(method, arguments, rawFactory: true); - } + factory _FakeMethodCall.fromFactory(String method, dynamic arguments) => + _FakeMethodCall(method, arguments, rawFactory: true); @override String toString() { @@ -259,7 +258,7 @@ void main() { const statement = 'SELECT COUNT(*) FROM `DemoModel`'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( where: [ WherePhrase([], isRequired: false), ], @@ -355,7 +354,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` COLLATE NOCASE'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'collate': 'NOCASE'}), + query: const Query(providerArgs: {'collate': 'NOCASE'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -367,7 +366,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` GROUP BY id'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'groupBy': 'id'}), + query: const Query(providerArgs: {'groupBy': 'id'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -379,7 +378,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` HAVING id'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'having': 'id'}), + query: const Query(providerArgs: {'having': 'id'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -391,7 +390,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(limit: 1), + query: const Query(limit: 1), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); @@ -403,7 +402,7 @@ void main() { const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` LIMIT 1 OFFSET 1'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query( + query: const Query( limit: 1, offset: 1, ), @@ -489,7 +488,7 @@ void main() { }); test('date time is converted', () async { - final statement = + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY datetime(simple_time) DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, @@ -506,7 +505,7 @@ void main() { // future Brick releases. // https://github.com/GetDutchie/brick/issues/429 test('incorrectly cased columns are forwarded as is', () async { - final statement = + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY complex_field_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, @@ -523,11 +522,11 @@ void main() { // guaranteed in future Brick releases. // https://github.com/GetDutchie/brick/issues/429 test('ordering by association is forwarded as is', () async { - final statement = + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY other_table.complex_field_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(providerArgs: {'orderBy': 'other_table.complex_field_name DESC'}), + query: const Query(providerArgs: {'orderBy': 'other_table.complex_field_name DESC'}), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); diff --git a/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart b/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart index 15adc264..2718ddf2 100644 --- a/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart +++ b/packages/brick_sqlite_generators/lib/src/sqlite_schema/sqlite_schema_generator.dart @@ -222,7 +222,7 @@ class SqliteSchemaGenerator { if (checker.isDartCoreType) { return SchemaColumn( column.name!, - Migration.fromDartPrimitive(checker.asPrimitive), + Column.fromDartPrimitive(checker.asPrimitive), nullable: column.nullable, unique: column.unique, ); From d94806bddaaa6cd33cd37c7f3390a29ffcb37630 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 14:39:44 -0800 Subject: [PATCH 2/5] revert dep bump --- packages/brick_sqlite/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/brick_sqlite/pubspec.yaml b/packages/brick_sqlite/pubspec.yaml index 891a016a..aa9c204f 100644 --- a/packages/brick_sqlite/pubspec.yaml +++ b/packages/brick_sqlite/pubspec.yaml @@ -10,7 +10,7 @@ environment: sdk: ">=2.18.0 <4.0.0" dependencies: - brick_core: ">=2.0.0 <3.0.0" + brick_core: ^1.1.1 collection: ">=1.0.0 <2.0.0" logging: ">=1.0.0 <2.0.0" meta: ">=1.3.0 <2.0.0" From 5e4ae6e568ae1a9ddc84e4b4435b381aab8fd325 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sat, 14 Dec 2024 21:36:48 -0800 Subject: [PATCH 3/5] 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); From 85b3d2a890495d53f77239d92bd1c89efb2b4717 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 15 Dec 2024 15:43:05 -0800 Subject: [PATCH 4/5] fix test --- ...first_apply_to_remote_deserialization.dart | 5 ++--- .../test_offline_first_where.dart | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart index 977a113f..43a1e278 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_apply_to_remote_deserialization.dart @@ -11,9 +11,8 @@ Future _$OfflineFirstWhereFromTest(Map data, OfflineFirstRepository? repository}) async { return OfflineFirstWhere( applied: await repository - ?.getAssociation(Query( - where: [Where.exact('id', data['id'])], - providerArgs: {'limit': 1})) + ?.getAssociation( + Query(where: [Where.exact('id', data['id'])], limit: 1)) .then((r) => r?.isNotEmpty ?? false ? r!.first : null), notApplied: data['not_applied'] == null ? null diff --git a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart index 681447d7..2bfc2c22 100644 --- a/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart +++ b/packages/brick_offline_first_build/test/offline_first_generator/test_offline_first_where.dart @@ -10,21 +10,22 @@ Future _$OfflineFirstWhereFromTest(Map data, {required TestProvider provider, OfflineFirstRepository? repository}) async { return OfflineFirstWhere( - assoc: - repository!.getAssociation(Query(where: [Where.exact('id', data['id'])], providerArgs: {'limit': 1})).then( - (r) => r!.first), + assoc: repository! + .getAssociation( + Query(where: [Where.exact('id', data['id'])], limit: 1)) + .then((r) => r!.first), assocs: (data['assocs'] ?? []) .map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s), Where.exact('otherVar', s)])).then( (r) => r!.first)) .toList(), - loadedAssoc: - await repository.getAssociation(Query(where: [Where.exact('id', data['id'])], providerArgs: {'limit': 1})).then( - (r) => r?.isNotEmpty ?? false ? r!.first : null), - loadedAssocs: - (await Future.wait((data['loaded_assocs'] ?? []).map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s)])).then((r) => r?.isNotEmpty ?? false ? r!.first : null)))) - .whereType() - .toList(), + loadedAssoc: await repository + .getAssociation( + Query(where: [Where.exact('id', data['id'])], limit: 1)) + .then((r) => r?.isNotEmpty ?? false ? r!.first : null), + loadedAssocs: (await Future.wait((data['loaded_assocs'] ?? []).map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s)])).then((r) => r?.isNotEmpty ?? false ? r!.first : null)))) + .whereType() + .toList(), multiLookupCustomGenerator: (data['multi_lookup_custom_generator'] ?? []) .map>((s) => repository.getAssociation(Query(where: [Where.exact('id', s), Where.exact('otherVar', s)])).then((r) => r!.first)) .toList()); From 4db2c475028eb8dfaafb1a60c000b886dd9d9b27 Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Sun, 15 Dec 2024 16:31:51 -0800 Subject: [PATCH 5/5] fix tests and add documentation --- MIGRATING.md | 30 +++++++- .../brick_core/lib/src/query/order_by.dart | 3 +- .../src/helpers/query_sql_transformer.dart | 28 +++++++- .../lib/src/sqlite_provider_query.dart | 6 ++ .../test/query_sql_transformer_test.dart | 69 +++++++++++-------- 5 files changed, 101 insertions(+), 35 deletions(-) diff --git a/MIGRATING.md b/MIGRATING.md index ec4007da..f9dbb030 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -6,16 +6,42 @@ Brick 4 away from loosely-defined `Query` arguments in favor of standardized fie ### Breaking Changes +**`providerArgs` will be supported until Brick 4 is officially released**. It is still recommended you migrate to the new `Query` for better `orderBy` and `limitBy`. + - `Query(providerArgs: {'limit':})` is now `Query(limit:)` - `Query(providerArgs: {'offset':})` is now `Query(offset:)` -- `Query(providerArgs: {'orderBy':})` is now `Query(orderBy:)`. This is a more significant change than `limit` or `offset`. `orderBy` is now defined by a class that permits multiple commands. For example, `'orderBy': 'name ASC'` becomes `[OrderBy('name', ascending: true)]`. -- `Query(providerArgs: {'restRequest':})` is now `Query(forProviders: [RestProviderQuery(request:)])`. This is a similarly significant chang that allows providers to be detected by static analysis and reduces the need for manual documentation. +- `Query(providerArgs: {'orderBy':})` is now `Query(orderBy:)`. This is a more significant change than `limit` or `offset`. `orderBy` is now defined by a class that permits multiple commands. For example, `'orderBy': 'name ASC'` becomes `[OrderBy('name', ascending: true)]`. First-class Brick providers (SQLite and Supabase) also support association-based querying by declaring a `model:`. + +#### brick_graphql + +- `Query(providerArgs: {'context':})` is now `Query(forProviders: [GraphqlProviderQuery(context:)])` +- `Query(providerArgs: {'operation':})` is now `Query(forProviders: [GraphqlProviderQuery(operation:)])` + +#### brick_rest + +- `Query(providerArgs: {'request':})` is now `Query(forProviders: [RestProviderQuery(request:)])`. This is a similarly significant chang that allows providers to be detected by static analysis and reduces the need for manual documentation. + +#### brick_sqlite + +- `Query(providerArgs: {'collate':})` is now `Query(forProviders: [SqliteProviderQuery(collate:)])` +- `Query(providerArgs: {'having':})` is now `Query(forProviders: [SqliteProviderQuery(having:)])` +- `Query(providerArgs: {'groupBy':})` is now `Query(forProviders: [SqliteProviderQuery(groupBy:)])` + +#### brick_supabase + +- `Query(providerArgs: {'limitReferencedTable':})` has been removed in favor of `Query(limitBy:)` +- `Query(providerArgs: {'orderByReferencedTable':})` has been removed in favor of `Query(orderBy:)` ### Improvements - `OrderBy` will support association ordering and multiple values - `Query` is constructed with `const` - `Query#offset` no longer requires companion `limit` parameter +- `brick_sqlite` and `brick_supabase` support association ordering. For example, `Query(orderBy: [OrderBy.desc('name', model: DemoModelAssoc)])` on `DemoModel` will produce the following SQL statement: + ```sql + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY `DemoModelAssoc`.name DESC' + ``` +- `brick_supabase` supports advanced limiting. For example, `Query(limitBy: [LimitBy(1, model: DemoModel))` is the equivalent of `.limit(1, referencedTable: 'demo_model')` ## Migrating from Brick 2 to Brick 3 diff --git a/packages/brick_core/lib/src/query/order_by.dart b/packages/brick_core/lib/src/query/order_by.dart index ace51d75..14d5276c 100644 --- a/packages/brick_core/lib/src/query/order_by.dart +++ b/packages/brick_core/lib/src/query/order_by.dart @@ -38,14 +38,13 @@ class OrderBy { factory OrderBy.fromJson(Map json) => OrderBy( json['evaluatedField'], ascending: json['ascending'], - model: json['model'], ); /// Serialize to JSON Map toJson() => { 'ascending': ascending, 'evaluatedField': evaluatedField, - if (model != null) 'model': model, + if (model != null) 'model': model?.toString(), }; @override 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 02956abb..d40a75b0 100644 --- a/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart +++ b/packages/brick_sqlite/lib/src/helpers/query_sql_transformer.dart @@ -93,6 +93,7 @@ class QuerySqlTransformer<_Model extends SqliteModel> { AllOtherClausesFragment( query, fieldsToColumns: adapter.fieldsToSqliteColumns, + modelDictionary: modelDictionary, ).toString(), ); } @@ -344,6 +345,9 @@ class AllOtherClausesFragment { /// final Map fieldsToColumns; + /// + final SqliteModelDictionary modelDictionary; + /// final Query? query; @@ -366,6 +370,7 @@ class AllOtherClausesFragment { AllOtherClausesFragment( this.query, { required this.fieldsToColumns, + required this.modelDictionary, }); @override @@ -377,7 +382,13 @@ class AllOtherClausesFragment { 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(', '), + 'orderBy': query?.orderBy + .map( + (p) => p.model != null + ? '`${modelDictionary.adapterFor[p.model]?.tableName}`.${modelDictionary.adapterFor[p.model]?.fieldsToSqliteColumns[p.evaluatedField]?.columnName} ${p.ascending ? 'ASC' : 'DESC'}' + : p.toString(), + ) + .join(', '), if (providerQuery?.groupBy != null) 'groupBy': providerQuery?.groupBy, if (providerQuery?.having != null) 'having': providerQuery?.having, }; @@ -391,6 +402,8 @@ class AllOtherClausesFragment { if (_operatorsDeclaringFields.contains(op)) { value = value.toString().split(',').fold(value.toString(), (modValue, innerValueClause) { + // TODO(tshedor): revisit and remove providerArgs hacks here after + // providerArgs is fully deprecated final fragment = innerValueClause.trim().split(' '); if (fragment.isEmpty) return modValue; @@ -404,6 +417,19 @@ class AllOtherClausesFragment { return modValue.replaceAll(fieldName, columnName); } + final tableFragment = innerValueClause.trim().split('.'); + if (fragment.isEmpty) return modValue; + + final tabledFieldName = tableFragment.last; + final tabledColumnDefinition = fieldsToColumns[fieldName]; + var tabledColumnName = tabledColumnDefinition?.columnName; + if (tabledColumnName != null && modValue.contains(fieldName)) { + if (columnDefinition!.type == DateTime) { + tabledColumnName = 'datetime($tabledColumnName)'; + } + return modValue.replaceAll(tabledFieldName, tabledColumnName); + } + return modValue; }); } diff --git a/packages/brick_sqlite/lib/src/sqlite_provider_query.dart b/packages/brick_sqlite/lib/src/sqlite_provider_query.dart index 803bd4ca..c0e5f586 100644 --- a/packages/brick_sqlite/lib/src/sqlite_provider_query.dart +++ b/packages/brick_sqlite/lib/src/sqlite_provider_query.dart @@ -1,13 +1,19 @@ import 'package:brick_core/query.dart'; import 'package:brick_sqlite/src/sqlite_provider.dart'; +/// [SqliteProvider]-specific options for a [Query] class SqliteProviderQuery extends ProviderQuery { + /// Defines a value for `COLLATE`. Often this field is used for case insensitive + /// queries where the value is `NOCASE`. final String? collate; + /// Defines a value for `GROUP BY`. final String? groupBy; + /// Defines a value for `HAVING`. final String? having; + /// [SqliteProvider]-specific options for a [Query] const SqliteProviderQuery({ this.collate, this.groupBy, diff --git a/packages/brick_sqlite/test/query_sql_transformer_test.dart b/packages/brick_sqlite/test/query_sql_transformer_test.dart index 8206089f..6e93df34 100644 --- a/packages/brick_sqlite/test/query_sql_transformer_test.dart +++ b/packages/brick_sqlite/test/query_sql_transformer_test.dart @@ -584,6 +584,18 @@ void main() { sqliteStatementExpectation(statement); }); + test('#limitBy is ignored', () async { + const statement = 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel`'; + final sqliteQuery = QuerySqlTransformer( + modelDictionary: dictionary, + query: const Query(limitBy: [LimitBy(1, model: DemoModelAssoc)]), + ); + 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( @@ -621,33 +633,34 @@ void main() { 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: Query(orderBy: [OrderBy.desc('manyAssoc')]), - ); - await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + test('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: Query(orderBy: [OrderBy.desc('manyAssoc')]), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); - expect(statement, sqliteQuery.statement); - sqliteStatementExpectation(statement); - }); + 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: Query( - orderBy: [OrderBy.desc('manyAssoc'), OrderBy.asc('complexFieldName')], - ), - ); - await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); + test('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: Query( + orderBy: [OrderBy.desc('manyAssoc'), OrderBy.asc('complexFieldName')], + ), + ); + await db.rawQuery(sqliteQuery.statement, sqliteQuery.values); - expect(statement, sqliteQuery.statement); - sqliteStatementExpectation(statement); + expect(statement, sqliteQuery.statement); + sqliteStatementExpectation(statement); + }); }); test('fields convert to column names in providerArgs values', () async { @@ -702,16 +715,12 @@ void main() { 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 { + test('ordering by association uses the specified model table', () async { const statement = - 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY other_table.complex_field_name DESC'; + 'SELECT DISTINCT `DemoModel`.* FROM `DemoModel` ORDER BY `DemoModelAssoc`.full_name DESC'; final sqliteQuery = QuerySqlTransformer( modelDictionary: dictionary, - query: Query(orderBy: [OrderBy.desc('other_table.complex_field_name')]), + query: Query(orderBy: [OrderBy.desc('name', model: DemoModelAssoc)]), ); await db.rawQuery(sqliteQuery.statement, sqliteQuery.values);