From 58925e8deea8a8a85dca74c9d64efeb4c37d9fe5 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:06:31 -0300 Subject: [PATCH 01/14] fix typo --- lib/src/bones_api_entity_db_postgres.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/bones_api_entity_db_postgres.dart b/lib/src/bones_api_entity_db_postgres.dart index 44a4eca1..f576faba 100644 --- a/lib/src/bones_api_entity_db_postgres.dart +++ b/lib/src/bones_api_entity_db_postgres.dart @@ -1093,13 +1093,13 @@ class DBPostgreSQLAdapter extends DBSQLAdapter return doInsertSQL( entityName, table, insertSQL, transaction, connection) - .resolveMapped((res) => _fixeTableSequence(transaction, entityName, + .resolveMapped((res) => _fixTableSequence(transaction, entityName, table, idFieldName, idFieldType, connection, res)); }); }); } - FutureOr _fixeTableSequence( + FutureOr _fixTableSequence( Transaction transaction, String entityName, String table, From 4ff3be6a37b107cd4a3d49f38fb1263e17cc954e Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:16:56 -0300 Subject: [PATCH 02/14] - New mixin `StatementCache` - New `CachedStatement`. --- lib/src/bones_api_entity_db_sql.dart | 141 +++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/lib/src/bones_api_entity_db_sql.dart b/lib/src/bones_api_entity_db_sql.dart index cb45d443..0202fb53 100644 --- a/lib/src/bones_api_entity_db_sql.dart +++ b/lib/src/bones_api_entity_db_sql.dart @@ -2950,3 +2950,144 @@ class DBSQLAdapterException extends DBAdapterException { super.operation, super.previousError}); } + +abstract mixin class StatementCache { + final Map>> _preparedSQLCache = {}; + + void disposePreparedSQLCache() { + _preparedSQLCache.clear(); + } + + FutureOr prepareSQLCached(String sql) { + var cached = _preparedSQLCache[sql]; + if (cached != null) { + return cached.useStatement(); + } + + checkPreparedSQLsCachedAsync(); + + try { + var future = _preparedSQLCache[sql] = prepareSQLCachedImpl(sql); + + if (future is Future>) { + future.catchError((e, s) { + _preparedSQLCache.remove(sql); + }); + } + + return future.statement; + } catch (_) { + _preparedSQLCache.remove(sql); + rethrow; + } + } + + FutureOr> prepareSQLCachedImpl(String sql); + + static const Duration defaultStatementCacheTimeout = Duration(minutes: 10); + + Duration statementCacheTimeout = defaultStatementCacheTimeout; + + Future checkPreparedSQLsCachedAsync( + {Duration delay = const Duration(seconds: 10), Duration? timeout}) { + return Future.delayed( + delay, () => checkPreparedSQLsCached(timeout: timeout)); + } + + DateTime _checkPreparedSQLsCachedTime = DateTime.now(); + + static final _checkPreparedSQLsCachedPeriod = const Duration(minutes: 1); + + int checkPreparedSQLsCached({bool force = false, Duration? timeout}) { + final now = DateTime.now(); + + if (!force) { + var checkElapsedTime = now.difference(_checkPreparedSQLsCachedTime); + if (checkElapsedTime < _checkPreparedSQLsCachedPeriod) { + return 0; + } + } + + _checkPreparedSQLsCachedTime = now; + + timeout ??= statementCacheTimeout; + + var expired = []; + + for (var e in _preparedSQLCache.entries) { + var cached = e.value; + if (cached is CachedStatement) { + if (cached.isExpired(now: now, timeout: timeout)) { + expired.add(e.key); + } + } + } + + for (var e in expired) { + var cached = _preparedSQLCache.remove(e); + if (cached is CachedStatement) { + disposeCachedStatement(cached.statement); + } + } + + return expired.length; + } + + void disposeCachedStatement(S statement); +} + +class CachedStatement { + final S statement; + + int _usageCount = 1; + DateTime _lastUsageTime; + + CachedStatement(this.statement) : _lastUsageTime = DateTime.now(); + + DateTime get lastUsageTime => _lastUsageTime; + + int get usageCount => _usageCount; + + void markUsage() { + ++_usageCount; + _lastUsageTime = DateTime.now(); + } + + S useStatement() { + markUsage(); + return statement; + } + + bool isExpired( + {DateTime? now, Duration timeout = const Duration(minutes: 10)}) { + now ??= DateTime.now(); + var elapsedTime = now.difference(_lastUsageTime); + return elapsedTime > timeout; + } +} + +extension FutureCachedStatementExtension on Future> { + Future get statement => then((r) => r.statement); + + Future useStatement() => then((r) => r.useStatement()); +} + +extension FutureOrCachedStatementExtension on FutureOr> { + FutureOr get statement { + var self = this; + if (self is Future>) { + return self.statement; + } else { + return self.statement; + } + } + + FutureOr useStatement() { + var self = this; + if (self is Future>) { + return self.useStatement(); + } else { + return self.useStatement(); + } + } +} From dd5c3176e44066ff5c64f734e061faf8242add67 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:17:27 -0300 Subject: [PATCH 03/14] - `DBSQLAdapterCapability`: added `statementsCache`. --- lib/src/bones_api_entity_db_sql.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/bones_api_entity_db_sql.dart b/lib/src/bones_api_entity_db_sql.dart index 0202fb53..1be03dca 100644 --- a/lib/src/bones_api_entity_db_sql.dart +++ b/lib/src/bones_api_entity_db_sql.dart @@ -283,12 +283,17 @@ class DBSQLAdapterCapability extends DBAdapterCapability { /// See [DBSQLAdapter.populateTables]. final bool tableSQL; + /// `true` if the adapter supports caching of SQL statements. + /// See [ConditionSQLEncoder.forCachedStatements] + final bool statementsCache; + const DBSQLAdapterCapability( {required super.dialect, required super.transactions, required super.transactionAbort, required super.constraintSupport, required this.tableSQL, + required this.statementsCache, required super.multiIsolateSupport, required super.connectivity}); @@ -434,8 +439,11 @@ abstract class DBSQLAdapter extends DBRelationalAdapter _populateTables = populateTables { boot(); - _conditionSQLGenerator = - ConditionSQLEncoder(this, sqlElementQuote: dialect.elementQuote); + _conditionSQLGenerator = ConditionSQLEncoder( + this, + sqlElementQuote: dialect.elementQuote, + forCachedStatements: capability.statementsCache, + ); } static FutureOr fromConfig>( From 2f648534cbf986e4cb319c870b1c53d97c5e7520 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:17:52 -0300 Subject: [PATCH 04/14] - `DBSQLAdapterCapability`: added `statementsCache`. --- lib/src/bones_api_entity_db_memory.dart | 1 + lib/src/bones_api_entity_db_mysql.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/src/bones_api_entity_db_memory.dart b/lib/src/bones_api_entity_db_memory.dart index cf4c282e..f6077a39 100644 --- a/lib/src/bones_api_entity_db_memory.dart +++ b/lib/src/bones_api_entity_db_memory.dart @@ -108,6 +108,7 @@ class DBSQLMemoryAdapter extends DBSQLAdapter transactions: true, transactionAbort: true, tableSQL: false, + statementsCache: false, constraintSupport: false, multiIsolateSupport: false, connectivity: DBAdapterCapabilityConnectivity.none), diff --git a/lib/src/bones_api_entity_db_mysql.dart b/lib/src/bones_api_entity_db_mysql.dart index f8bb8e15..81c51de3 100644 --- a/lib/src/bones_api_entity_db_mysql.dart +++ b/lib/src/bones_api_entity_db_mysql.dart @@ -104,6 +104,7 @@ class DBMySQLAdapter extends DBSQLAdapter transactions: true, transactionAbort: true, tableSQL: true, + statementsCache: false, constraintSupport: false, multiIsolateSupport: true, connectivity: DBAdapterCapabilityConnectivity.secureAndUnsecure), From 8b531b6a4dedcc67b87a6ed019c1213716b07f84 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:18:47 -0300 Subject: [PATCH 05/14] - `ConditionSQLEncoder`: - Added property `forCachedStatements`: forces generation of SQL statements suitable for caching. --- lib/src/bones_api_condition_sql.dart | 43 +++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/src/bones_api_condition_sql.dart b/lib/src/bones_api_condition_sql.dart index 6766fdeb..087a15bc 100644 --- a/lib/src/bones_api_condition_sql.dart +++ b/lib/src/bones_api_condition_sql.dart @@ -9,10 +9,15 @@ final _log = logging.Logger('ConditionSQLEncoder'); /// A [Condition] encoder for SQL. class ConditionSQLEncoder extends ConditionEncoder { + /// The character used to quote identifiers in the SQL dialect. final String sqlElementQuote; + /// If `true`, forces generation of SQL statements suitable for caching by + /// avoiding inline values and using substitution values whenever possible. + final bool forCachedStatements; + ConditionSQLEncoder(SchemeProvider super.schemeProvider, - {required this.sqlElementQuote}); + {required this.sqlElementQuote, this.forCachedStatements = false}); @override String get groupOpener => '('; @@ -38,6 +43,15 @@ class ConditionSQLEncoder extends ConditionEncoder { var schemeProvider = this.schemeProvider; if (schemeProvider == null) { var idKey = context.addEncodingParameter('id', c.idValue); + + if (forCachedStatements) { + var c2 = KeyConditionEQ( + [ConditionKeyField('id')], + ConditionParameter.key(idKey), + ); + return encodeKeyConditionEQ(c2, context); + } + var q = sqlElementQuote; var tableKey = '$q$tableAlias$q.$q$idKey$q'; @@ -58,6 +72,15 @@ class ConditionSQLEncoder extends ConditionEncoder { var idType = tableScheme.fieldsTypes[idFieldName] ?? int; var idKey = context.addEncodingParameter(idFieldName, c.idValue); + + if (forCachedStatements) { + var c2 = KeyConditionEQ( + [ConditionKeyField(idFieldName)], + ConditionParameter.key(idKey), + ); + return encodeKeyConditionEQ(c2, context); + } + var q = sqlElementQuote; var tableKey = '$q$tableAlias$q.$q$idKey$q'; @@ -76,6 +99,15 @@ class ConditionSQLEncoder extends ConditionEncoder { var schemeProvider = this.schemeProvider; if (schemeProvider == null) { var idKey = context.addEncodingParameter('id', c.idsValues); + + if (forCachedStatements) { + var c2 = KeyConditionIN( + [ConditionKeyField('id')], + ConditionParameter.key(idKey), + ); + return encodeKeyConditionIN(c2, context); + } + var q = sqlElementQuote; var tableKey = '$q$tableAlias$q.$q$idKey$q'; @@ -96,6 +128,15 @@ class ConditionSQLEncoder extends ConditionEncoder { var idType = tableScheme.fieldsTypes[idFieldName] ?? int; var idKey = context.addEncodingParameter(idFieldName, c.idsValues); + + if (forCachedStatements) { + var c2 = KeyConditionIN( + [ConditionKeyField(idFieldName)], + ConditionParameter.key(idKey), + ); + return encodeKeyConditionIN(c2, context); + } + var q = sqlElementQuote; var tableKey = '$q$tableAlias$q.$q$idKey$q'; From e196413dd98ca181b423269a89ecb28ae66b8d81 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:26:29 -0300 Subject: [PATCH 06/14] Dart docs for: StatementCache and CachedStatement --- lib/src/bones_api_entity_db_sql.dart | 50 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/src/bones_api_entity_db_sql.dart b/lib/src/bones_api_entity_db_sql.dart index 1be03dca..baa6ae46 100644 --- a/lib/src/bones_api_entity_db_sql.dart +++ b/lib/src/bones_api_entity_db_sql.dart @@ -2958,14 +2958,26 @@ class DBSQLAdapterException extends DBAdapterException { super.operation, super.previousError}); } - +/// A mixin that provides SQL statement caching functionality. +/// +/// `StatementCache` manages a cache of prepared SQL statements, +/// automatically expiring and disposing of them after a configurable timeout. abstract mixin class StatementCache { + /// Internal cache mapping SQL strings to prepared statements. final Map>> _preparedSQLCache = {}; + /// Clears all cached prepared statements. void disposePreparedSQLCache() { _preparedSQLCache.clear(); } + /// Prepares or retrieves a cached SQL statement. + /// + /// If the statement is already cached, it's reused. Otherwise, + /// [prepareSQLCachedImpl] is called to prepare a new statement, + /// which is cached and returned. + /// + /// If an error occurs during preparation, the entry is removed from the cache. FutureOr prepareSQLCached(String sql) { var cached = _preparedSQLCache[sql]; if (cached != null) { @@ -2990,22 +3002,40 @@ abstract mixin class StatementCache { } } + /// Prepares and returns a [CachedStatement] for the given SQL. + /// + /// Must be implemented by the concrete class. FutureOr> prepareSQLCachedImpl(String sql); + /// Default timeout (10 min) used to determine statement expiration. static const Duration defaultStatementCacheTimeout = Duration(minutes: 10); + /// Configurable timeout for cached statements. Default: 10 min Duration statementCacheTimeout = defaultStatementCacheTimeout; + /// Schedules an asynchronous check for expired cached statements. + /// + /// [delay] specifies the delay before the check is performed. Default: 10 sec + /// [timeout] overrides the default expiration duration. Future checkPreparedSQLsCachedAsync( {Duration delay = const Duration(seconds: 10), Duration? timeout}) { return Future.delayed( delay, () => checkPreparedSQLsCached(timeout: timeout)); } - DateTime _checkPreparedSQLsCachedTime = DateTime.now(); + + /// Minimum interval between automatic cache checks (1 min). static final _checkPreparedSQLsCachedPeriod = const Duration(minutes: 1); + /// Tracks the last time cache was checked. + DateTime _checkPreparedSQLsCachedTime = DateTime.now(); + + /// Checks for and removes expired cached statements. + /// + /// Returns the number of removed statements. + /// + /// Set [force] to `true` to force a check regardless of elapsed time. int checkPreparedSQLsCached({bool force = false, Duration? timeout}) { final now = DateTime.now(); @@ -3041,31 +3071,47 @@ abstract mixin class StatementCache { return expired.length; } + /// Disposes of the given statement. + /// + /// Must be implemented to properly clean up resources. void disposeCachedStatement(S statement); } +/// A wrapper for a cached prepared SQL statement of generic type `S`. +/// +/// Tracks usage statistics and expiration status. class CachedStatement { + /// The actual prepared statement instance. final S statement; int _usageCount = 1; DateTime _lastUsageTime; + /// Creates a [CachedStatement] for the given [statement]. CachedStatement(this.statement) : _lastUsageTime = DateTime.now(); + /// Timestamp of the last time the statement was used. DateTime get lastUsageTime => _lastUsageTime; + /// Number of times the statement was used. int get usageCount => _usageCount; + /// Marks the statement as used, updating the usage count and timestamp. void markUsage() { ++_usageCount; _lastUsageTime = DateTime.now(); } + /// Returns the statement, marking it as used. + /// See [markUsage]. S useStatement() { markUsage(); return statement; } + /// Returns `true` if the statement is considered expired. + /// + /// Uses [timeout] to determine expiration based on last usage time. bool isExpired( {DateTime? now, Duration timeout = const Duration(minutes: 10)}) { now ??= DateTime.now(); From 574b395725beb4771595574854cf301000379c7a Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:26:48 -0300 Subject: [PATCH 07/14] dart format --- lib/src/bones_api_entity_db_sql.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/bones_api_entity_db_sql.dart b/lib/src/bones_api_entity_db_sql.dart index baa6ae46..cbad1a35 100644 --- a/lib/src/bones_api_entity_db_sql.dart +++ b/lib/src/bones_api_entity_db_sql.dart @@ -2958,6 +2958,7 @@ class DBSQLAdapterException extends DBAdapterException { super.operation, super.previousError}); } + /// A mixin that provides SQL statement caching functionality. /// /// `StatementCache` manages a cache of prepared SQL statements, @@ -3023,8 +3024,6 @@ abstract mixin class StatementCache { delay, () => checkPreparedSQLsCached(timeout: timeout)); } - - /// Minimum interval between automatic cache checks (1 min). static final _checkPreparedSQLsCachedPeriod = const Duration(minutes: 1); From 3d7bbd0444a624bde443499e33071dbc40bee2e2 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:27:41 -0300 Subject: [PATCH 08/14] - `PostgreSQLConnectionWrapper` now has support for statements cache (with `StatementCache`). --- lib/src/bones_api_entity_db_postgres.dart | 36 +++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/src/bones_api_entity_db_postgres.dart b/lib/src/bones_api_entity_db_postgres.dart index f576faba..c77b53f9 100644 --- a/lib/src/bones_api_entity_db_postgres.dart +++ b/lib/src/bones_api_entity_db_postgres.dart @@ -111,6 +111,7 @@ class DBPostgreSQLAdapter extends DBSQLAdapter transactions: true, transactionAbort: true, tableSQL: true, + statementsCache: true, constraintSupport: true, multiIsolateSupport: true, connectivity: DBAdapterCapabilityConnectivity.secureAndUnsecure), @@ -1215,7 +1216,8 @@ class DBPostgreSQLAdapter extends DBSQLAdapter } /// A [DBPostgreSQLAdapter] connection wrapper. -class PostgreSQLConnectionWrapper extends DBConnectionWrapper { +class PostgreSQLConnectionWrapper extends DBConnectionWrapper + with StatementCache { final String? username; final String host; final int port; @@ -1230,14 +1232,17 @@ class PostgreSQLConnectionWrapper extends DBConnectionWrapper { return "postgresql://$username@$host:$port/$database${secure ? '?sslmode=require' : ''}"; } + Future _executeWithCachedStatement( + String sql, Map? substitutionValues) async { + var statement = await prepareSQLCached(sql); + return statement.run(substitutionValues); + } + Future>> mappedResultsQuery(String sql, {Map? substitutionValues}) async { updateLastAccessTime(); - var rs = await nativeConnection.execute( - Sql.named(sql), - parameters: substitutionValues, - ); + var rs = await _executeWithCachedStatement(sql, substitutionValues); var mappedResult = rs.map((e) => e.toResultsMap()).toList(); @@ -1247,20 +1252,21 @@ class PostgreSQLConnectionWrapper extends DBConnectionWrapper { Future query(String sql, {Map? substitutionValues}) async { updateLastAccessTime(); - return nativeConnection.execute( - Sql.named(sql), - parameters: substitutionValues, - ); + + return _executeWithCachedStatement(sql, substitutionValues); } Future execute(String sql, {Map? substitutionValues}) async { updateLastAccessTime(); + + // Can't use `prepareSQLCached` for `ignoreRows: true`. var rs = await nativeConnection.execute( Sql.named(sql), parameters: substitutionValues, ignoreRows: true, ); + return rs.affectedRows; } @@ -1280,6 +1286,18 @@ class PostgreSQLConnectionWrapper extends DBConnectionWrapper { ); } + @override + Future> prepareSQLCachedImpl(String sql) async { + var sqlNamed = Sql.named(sql); + var statement = await nativeConnection.prepare(sqlNamed); + return CachedStatement(statement); + } + + @override + void disposeCachedStatement(Statement statement) { + statement.dispose(); + } + @override bool isClosedImpl() { final nativeConnection = this.nativeConnection; From c07a957b8f783059c24f6575c084f7e87cf9247e Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 18:27:57 -0300 Subject: [PATCH 09/14] v1.9.4-beta.1 - `DBSQLAdapterCapability`: added `statementsCache`. - `ConditionSQLEncoder`: - Added property `forCachedStatements`: forces generation of SQL statements suitable for caching. - New mixin `StatementCache` - New `CachedStatement`. - `PostgreSQLConnectionWrapper` now has support for statements cache (with `StatementCache`). --- CHANGELOG.md | 12 ++++++++++++ lib/src/bones_api_base.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a2c125..d24b2849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.9.4-beta.1 + +- `DBSQLAdapterCapability`: added `statementsCache`. + +- `ConditionSQLEncoder`: + - Added property `forCachedStatements`: forces generation of SQL statements suitable for caching. + +- New mixin `StatementCache` + - New `CachedStatement`. + +- `PostgreSQLConnectionWrapper` now has support for statements cache (with `StatementCache`). + ## 1.9.3-beta.11 - `DBEntityRepositoryProvider`: diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index 37325f1b..8c03da1c 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -42,7 +42,7 @@ typedef APILogger = void Function(APIRoot apiRoot, String type, String? message, /// Bones API Library class. class BonesAPI { // ignore: constant_identifier_names - static const String VERSION = '1.9.3-beta.11'; + static const String VERSION = '1.9.4-beta.1'; static bool _boot = false; diff --git a/pubspec.yaml b/pubspec.yaml index 9cd06cf0..ca5c0c4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - A powerful API backend framework for Dart. It comes with a built-in HTTP Server, route handler, entity handler, SQL translator, and DB adapters. -version: 1.9.3-beta.11 +version: 1.9.4-beta.1 homepage: https://github.com/Colossus-Services/bones_api environment: From f2ee0759e0f03a3d62b7de4a3f8f2a6c10b17663 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 20:12:59 -0300 Subject: [PATCH 10/14] fix lint --- lib/src/bones_api_entity_db_sql.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/bones_api_entity_db_sql.dart b/lib/src/bones_api_entity_db_sql.dart index cbad1a35..9cedacf6 100644 --- a/lib/src/bones_api_entity_db_sql.dart +++ b/lib/src/bones_api_entity_db_sql.dart @@ -2991,8 +2991,9 @@ abstract mixin class StatementCache { var future = _preparedSQLCache[sql] = prepareSQLCachedImpl(sql); if (future is Future>) { - future.catchError((e, s) { + future.then((_) => null, onError: (_) { _preparedSQLCache.remove(sql); + return null; }); } From 1ababbe453388e4fa6ed891bedcce3c384f4089e Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 20:35:23 -0300 Subject: [PATCH 11/14] bones_api_entity_db_tests_base.dart --- test/bones_api_entity_db_tests_base.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/bones_api_entity_db_tests_base.dart b/test/bones_api_entity_db_tests_base.dart index 711a9539..3479ed88 100644 --- a/test/bones_api_entity_db_tests_base.dart +++ b/test/bones_api_entity_db_tests_base.dart @@ -866,9 +866,19 @@ Future runAdapterTests( expect(await userAPIRepository.existsID(1), isTrue); expect(await userAPIRepository.existsID(id), isTrue); - expect(await userAPIRepository.existsID(123123123123), isFalse); - expect(await userAPIRepository.existIDs([1, id, 123123123123]), - equals([1, id])); + + // Named parameters in SQL: + if (sqlAdapter.capability.statementsCache) { + expect(await userAPIRepository.existsID(1231231231), isFalse); + expect(await userAPIRepository.existIDs([1, id, 1231231231]), + equals([1, id])); + } + // In-line values in SQL: + else { + expect(await userAPIRepository.existsID(123123123123), isFalse); + expect(await userAPIRepository.existIDs([1, id, 123123123123]), + equals([1, id])); + } expect( await userAPIRepository.selectIDsByQuery(' email == ? ', From 18fb6298f3c0a7f9889a0a993e26461c77b31343 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 29 Jun 2025 22:41:53 -0300 Subject: [PATCH 12/14] typo --- lib/src/bones_api_condition_encoder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/bones_api_condition_encoder.dart b/lib/src/bones_api_condition_encoder.dart index 7eb894ae..6cb33d5e 100644 --- a/lib/src/bones_api_condition_encoder.dart +++ b/lib/src/bones_api_condition_encoder.dart @@ -1273,7 +1273,7 @@ abstract class ConditionEncoder { value == list.first; } else { throw ArgumentError( - "Can't resolve a `List` with mutiple values to a primitive type: $valueTypeInfo >> $value"); + "Can't resolve a `List` with multiple values to a primitive type: $valueTypeInfo >> $value"); } } From 0fe2eb6b74453d665a08d7323f3ebc31db447558 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Mon, 30 Jun 2025 16:51:28 -0300 Subject: [PATCH 13/14] - `ConditionSQLEncoder`: - Fix resolution of parameter values present only in `EncodingContext.encodingParameters` (i.e., generated by the encoder, not passed as normal parameters). --- lib/src/bones_api_condition_encoder.dart | 24 ++++++++++++++ lib/src/bones_api_condition_sql.dart | 42 ++++++++++++++++++------ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/src/bones_api_condition_encoder.dart b/lib/src/bones_api_condition_encoder.dart index 6cb33d5e..528b599f 100644 --- a/lib/src/bones_api_condition_encoder.dart +++ b/lib/src/bones_api_condition_encoder.dart @@ -1158,6 +1158,10 @@ abstract class ConditionEncoder { if (valueAsList) { return _resolveParameterValueImpl(value, context, valueType, true) .resolveMapped((values) { + if (values is EncodingValue) { + return values.asEncodingValueList(); + } + var list = values is List ? values : (values is Iterable ? values.toList(growable: false) : [values]); @@ -1179,6 +1183,10 @@ abstract class ConditionEncoder { if (!context.parametersPlaceholders.containsKey(valueKey)) { return _resolveParameterValueImpl(value, context, valueType, false) .resolveMapped((val) { + if (val is EncodingValue) { + return val; + } + context.parametersPlaceholders[valueKey] ??= val; return EncodingPlaceholder( valueKey, valueType, placeholder, encodeEncodingPlaceholder); @@ -1198,6 +1206,10 @@ abstract class ConditionEncoder { namedParameters: context.namedParameters, encodingParameters: context.encodingParameters); + if (paramValue is EncodingValue) { + return paramValue; + } + if (valueType != null) { return resolveValueToType(paramValue, valueType, valueAsList: valueAsList); @@ -1354,6 +1366,18 @@ abstract class EncodingValue> { @override String toString() => encode.toString(); + + EncodingValueList asEncodingValueList() { + var values = this; + if (values is EncodingValueList) { + return values as EncodingValueList; + } else { + return EncodingValueList(values.key, values.type, [values], (p) { + var v = values.encode; + return '( $v )' as E; + }); + } + } } abstract class EncodingValueResolved> diff --git a/lib/src/bones_api_condition_sql.dart b/lib/src/bones_api_condition_sql.dart index 087a15bc..5d440a01 100644 --- a/lib/src/bones_api_condition_sql.dart +++ b/lib/src/bones_api_condition_sql.dart @@ -42,7 +42,12 @@ class ConditionSQLEncoder extends ConditionEncoder { var schemeProvider = this.schemeProvider; if (schemeProvider == null) { - var idKey = context.addEncodingParameter('id', c.idValue); + var idValue = c.idValue; + + var encodingValue = valueToParameterValue(context, idValue, + fieldKey: 'id', fieldType: int); + + var idKey = context.addEncodingParameter('id', encodingValue); if (forCachedStatements) { var c2 = KeyConditionEQ( @@ -56,7 +61,7 @@ class ConditionSQLEncoder extends ConditionEncoder { var tableKey = '$q$tableAlias$q.$q$idKey$q'; return encodeConditionValuesWithOperator( - context, int, idKey, tableKey, '=', c.idValue, false); + context, int, idKey, tableKey, '=', encodingValue, false); } else { var tableSchemeRet = schemeProvider.getTableScheme(tableName); @@ -70,8 +75,12 @@ class ConditionSQLEncoder extends ConditionEncoder { var idFieldName = tableScheme.idFieldName ?? 'id'; var idType = tableScheme.fieldsTypes[idFieldName] ?? int; + var idValue = c.idValue; - var idKey = context.addEncodingParameter(idFieldName, c.idValue); + var encodingValue = valueToParameterValue(context, idValue, + fieldKey: idFieldName, fieldType: idType); + + var idKey = context.addEncodingParameter(idFieldName, encodingValue); if (forCachedStatements) { var c2 = KeyConditionEQ( @@ -85,7 +94,7 @@ class ConditionSQLEncoder extends ConditionEncoder { var tableKey = '$q$tableAlias$q.$q$idKey$q'; return encodeConditionValuesWithOperator( - context, idType, idKey, tableKey, '=', c.idValue, false); + context, idType, idKey, tableKey, '=', encodingValue, false); }); } } @@ -98,7 +107,12 @@ class ConditionSQLEncoder extends ConditionEncoder { var schemeProvider = this.schemeProvider; if (schemeProvider == null) { - var idKey = context.addEncodingParameter('id', c.idsValues); + var idsValues = c.idsValues; + + var encodingValue = valueToParameterValue(context, idsValues, + fieldKey: 'id', fieldType: int, valueAsList: true); + + var idKey = context.addEncodingParameter('id', encodingValue); if (forCachedStatements) { var c2 = KeyConditionIN( @@ -112,7 +126,7 @@ class ConditionSQLEncoder extends ConditionEncoder { var tableKey = '$q$tableAlias$q.$q$idKey$q'; return encodeConditionValuesWithOperator( - context, int, idKey, tableKey, 'IN', c.idsValues, true); + context, int, idKey, tableKey, 'IN', encodingValue, true); } else { var tableSchemeRet = schemeProvider.getTableScheme(tableName); @@ -126,8 +140,12 @@ class ConditionSQLEncoder extends ConditionEncoder { var idFieldName = tableScheme.idFieldName ?? 'id'; var idType = tableScheme.fieldsTypes[idFieldName] ?? int; + var idsValues = c.idsValues; + + var encodingValue = valueToParameterValue(context, idsValues, + fieldKey: idFieldName, fieldType: idType, valueAsList: true); - var idKey = context.addEncodingParameter(idFieldName, c.idsValues); + var idKey = context.addEncodingParameter(idFieldName, encodingValue); if (forCachedStatements) { var c2 = KeyConditionIN( @@ -141,7 +159,7 @@ class ConditionSQLEncoder extends ConditionEncoder { var tableKey = '$q$tableAlias$q.$q$idKey$q'; return encodeConditionValuesWithOperator( - context, idType, idFieldName, tableKey, 'IN', c.idsValues, true); + context, idType, idFieldName, tableKey, 'IN', encodingValue, true); }); } } @@ -550,14 +568,18 @@ class ConditionSQLEncoder extends ConditionEncoder { } } - if (value is ConditionParameter) { + if (value is EncodingValue) { + return value; + } else if (value is ConditionParameter) { return conditionParameterToParameterValue( value, context, fieldKey, fieldType, valueAsList: valueAsList); } else if (value is List && value.whereType().isNotEmpty) { var parametersValues = value.map((v) { - if (v is ConditionParameter) { + if (value is EncodingValue) { + return value; + } else if (v is ConditionParameter) { return conditionParameterToParameterValue( value, context, fieldKey, fieldType, valueAsList: valueAsList); From abd5aa65497472a9c37e4a73a6fc9286bada63dd Mon Sep 17 00:00:00 2001 From: gmpassos Date: Mon, 30 Jun 2025 16:51:44 -0300 Subject: [PATCH 14/14] - `ConditionSQLEncoder`: - Fix resolution of parameter values present only in `EncodingContext.encodingParameters` (i.e., generated by the encoder, not passed as normal parameters). --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7ea7dd..f17c730c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - `ConditionSQLEncoder`: - Added property `forCachedStatements`: forces generation of SQL statements suitable for caching. + - Fix resolution of parameter values present only in `EncodingContext.encodingParameters` + (i.e., generated by the encoder, not passed as normal parameters). - New mixin `StatementCache` - New `CachedStatement`.