diff --git a/CHANGELOG.md b/CHANGELOG.md index 69d0314..3248326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.15.2 + +- The placeholder pruning added in 1.15.1 no longer runs on every encoded + condition. + + Rewriting `field == ?` bound to null into `field IS NULL` leaves that + parameter unreferenced, and 1.15.1 found it by materializing the encoded + output and scanning it for *every* placeholder. That cost was paid by every + query, including the overwhelmingly common one that compares nothing against + null: **~0.30us per encoded condition** (measured on a 3-placeholder + condition), against the ~0.90us of a whole logged route call after the 1.15.0 + dispatch work. + + Only a placeholder actually rewritten to `IS NULL`/`IS NOT NULL` can become + unreferenced, so those keys are now recorded as they are written, and a + condition that compares nothing against null returns immediately — without + materializing the output or scanning it. The output is materialized lazily + even then, since a recorded key may still be referenced by another operator. + + No behaviour change: same statements, same bound parameters. + ## 1.15.1 - Fixed: a condition comparing a field to `null` **passed as a parameter** was diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index 87f9c3e..80415e5 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -48,7 +48,7 @@ typedef APILogger = /// Bones API Library class. class BonesAPI { // ignore: constant_identifier_names - static const String VERSION = '1.15.1'; + static const String VERSION = '1.15.2'; static bool _boot = false; diff --git a/lib/src/bones_api_condition_encoder.dart b/lib/src/bones_api_condition_encoder.dart index c040a0c..b37e920 100644 --- a/lib/src/bones_api_condition_encoder.dart +++ b/lib/src/bones_api_condition_encoder.dart @@ -926,6 +926,22 @@ class EncodingContext { /// The encoded parameters placeholders and values. final Map parametersPlaceholders = {}; + /// The placeholders whose comparison was written as `IS NULL`/`IS NOT NULL` + /// instead of as the placeholder itself. Only these can end up resolved but + /// unreferenced, so only these are worth looking for in the output. + /// + /// Left null while empty: the common encoding compares nothing against null, + /// and allocating a `Set` per encoded condition is not free. + Set? _nullInlinedPlaceholders; + + /// Marks [parameterKey] as having been written as SQL `NULL` inline. + void markPlaceholderInlinedAsNull(String parameterKey) => + (_nullInlinedPlaceholders ??= {}).add(parameterKey); + + /// The keys marked by [markPlaceholderInlinedAsNull]. + Iterable get nullInlinedPlaceholders => + _nullInlinedPlaceholders ?? const {}; + /// The table aliases used in the encoded output. final Map tableAliases = {}; @@ -1210,15 +1226,30 @@ abstract class ConditionEncoder { /// rather than as the placeholder, which leaves its parameter resolved but /// unmentioned by the statement. PostgreSQL rejects a statement carrying /// variables it does not use, so the entry has to go. + /// + /// Only a placeholder rewritten that way can become unreferenced, so a + /// condition that compares nothing against null returns here immediately — + /// without materializing the output or scanning it. void pruneUnusedParametersPlaceholders(EncodingContext context) { + var nullInlined = context.nullInlinedPlaceholders; + if (nullInlined.isEmpty) return; + var parametersPlaceholders = context.parametersPlaceholders; if (parametersPlaceholders.isEmpty) return; - var output = context.outputString; + // Resolved lazily: a marked key may still be referenced by another + // operator, and then there is nothing to scan for. + String? output; - parametersPlaceholders.removeWhere( - (key, _) => !_isPlaceholderInOutput(output, parameterPlaceholder(key)), - ); + for (var key in nullInlined) { + if (!parametersPlaceholders.containsKey(key)) continue; + + output ??= context.outputString; + + if (!_isPlaceholderInOutput(output, parameterPlaceholder(key))) { + parametersPlaceholders.remove(key); + } + } } static bool _isPlaceholderInOutput(String output, String placeholder) { diff --git a/lib/src/bones_api_condition_sql.dart b/lib/src/bones_api_condition_sql.dart index d33b024..936d8b9 100644 --- a/lib/src/bones_api_condition_sql.dart +++ b/lib/src/bones_api_condition_sql.dart @@ -271,12 +271,14 @@ class ConditionSQLEncoder extends ConditionEncoder { case 'IN': { context.write('IS NULL '); + _markPlaceholderInlinedAsNull(value, context); return context; } case '!=': case 'NOT IN': { context.write('IS NOT NULL '); + _markPlaceholderInlinedAsNull(value, context); return context; } } @@ -290,6 +292,20 @@ class ConditionSQLEncoder extends ConditionEncoder { }); } + /// Records that [value]'s placeholder was written as `IS NULL`/`IS NOT NULL`, + /// so its parameter can be dropped if nothing else references it. + /// + /// Only a placeholder is registered as a parameter; an inlined + /// [EncodingValueNull] never was, so it has nothing to drop. + void _markPlaceholderInlinedAsNull( + EncodingValue value, + EncodingContext context, + ) { + if (value is EncodingPlaceholder) { + context.markPlaceholderInlinedAsNull(value.key); + } + } + /// Whether [value] is a comparison against SQL `NULL`. /// /// A null written straight into the statement arrives as an diff --git a/pubspec.yaml b/pubspec.yaml index 5f73741..4317e92 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.15.1 +version: 1.15.2 homepage: https://github.com/Colossus-Services/bones_api environment: