From 339ee9df4ce304310879f71e188057eeae556a4a Mon Sep 17 00:00:00 2001 From: "Graciliano M. P." Date: Wed, 12 Aug 2026 07:16:04 -0300 Subject: [PATCH] perf: prune placeholders only when a null was inlined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.15.1 rewrites `field == ?` bound to null into `field IS NULL`, which leaves that parameter unreferenced, and found it by materializing the encoded output and scanning it for every placeholder. Every query paid that, including the overwhelmingly common one comparing nothing against null: ~0.30us per encoded condition (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 `EncodingContext` now records those keys as they are written, and the prune returns immediately when the set is empty — no `toString()`, no scan. The output is materialized lazily even when it is non-empty, since a recorded key may still be referenced by another operator (`field > ?` bound to null), and then there is nothing to look for. The set is left null while empty: allocating one per encoded condition would give back part of what this saves. No behaviour change: same statements, same bound parameters. Verified by the suites that caught the original bug — PostgreSQL 65/65 and MySQL 65/65, where an unreferenced parameter is a hard error ("Contains superfluous variables"). Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 21 +++++++++++++ lib/src/bones_api_base.dart | 2 +- lib/src/bones_api_condition_encoder.dart | 39 +++++++++++++++++++++--- lib/src/bones_api_condition_sql.dart | 16 ++++++++++ pubspec.yaml | 2 +- 5 files changed, 74 insertions(+), 6 deletions(-) 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: