Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
39 changes: 35 additions & 4 deletions lib/src/bones_api_condition_encoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,22 @@ class EncodingContext {
/// The encoded parameters placeholders and values.
final Map<String, dynamic> parametersPlaceholders = <String, dynamic>{};

/// 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<String>? _nullInlinedPlaceholders;

/// Marks [parameterKey] as having been written as SQL `NULL` inline.
void markPlaceholderInlinedAsNull(String parameterKey) =>
(_nullInlinedPlaceholders ??= <String>{}).add(parameterKey);

/// The keys marked by [markPlaceholderInlinedAsNull].
Iterable<String> get nullInlinedPlaceholders =>
_nullInlinedPlaceholders ?? const <String>{};

/// The table aliases used in the encoded output.
final Map<String, String> tableAliases = <String, String>{};

Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions lib/src/bones_api_condition_sql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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<String, Object?> 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
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down