Skip to content

Convert string literals to the operand type at prepare time in comparisons - #9108

Open
ChudaykinAlex wants to merge 1 commit into
FirebirdSQL:masterfrom
ChudaykinAlex:work/optimization_literal_conversion
Open

Convert string literals to the operand type at prepare time in comparisons#9108
ChudaykinAlex wants to merge 1 commit into
FirebirdSQL:masterfrom
ChudaykinAlex:work/optimization_literal_conversion

Conversation

@ChudaykinAlex

@ChudaykinAlex ChudaykinAlex commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

In a comparison between a typed column and a string literal — col >= '2018-07-01',
col BETWEEN 'lo' AND 'hi' — the literal stays a dtype_text node all the way to
execution. The string→type conversion is then performed once per row, even though
the literal is constant and its target type is known at prepare time.

On a 2 000 000-row full scan this is a measurable share of the total time, and it is
worst for date/time types, where parsing a string into a timestamp is expensive.

Change

ComparativeBoolNode::dsqlPass() now converts a string literal operand into a typed
constant once, at prepare time, using the descriptor of the opposite operand as the
target type.

  • New helper MAKE_constant_from_literal(LiteralNode*, const dsc*) in src/dsql/make.cpp.
  • Applied to arg1 vs arg2, arg2 vs arg1, and — for blr_between — to arg3
    (the upper bound) vs arg1.
  • Supported target types: DOUBLE, DECFLOAT(16), DECFLOAT(34), INT128,
    DATE, TIME, TIME WITH TIME ZONE, TIMESTAMP, TIMESTAMP WITH TIME ZONE.
    Anything else is left alone.

Why this is safe

Two guards keep the optimization on exactly the path the runtime would have taken
anyway.

1. Only when the runtime would convert in the same direction.

if (CVT2_compare_priority[from->litDesc.dsc_dtype] >=
    CVT2_compare_priority[reference->dsc_dtype])
{
    return nullptr;
}

The conversion is performed only when the reference operand has a strictly higher
comparison priority — i.e. the same coercion CVT2_compare would apply per row. If
the comparison is textual (string vs string, or string vs a lower-priority type), the
node is left untouched. This is what keeps '2000-01-01' = '2000-01-01 ' behaving as
a string comparison rather than silently becoming a date comparison.

2. Only for dtype_text operands.

The helper returns early unless the literal is dtype_text, so nothing happens for
already-typed literals, parameters, or expressions.

Regressions found during review and fixed in this branch

Both were caught by the existing QA suite and are fixed in the commits below.

1 = -:id failed with "Invalid data type for negation" (core_5395).

The first iteration forced DsqlDescMaker::fromNode() on the opposite operand for
any literal, including numeric ones. For 1 = -:id that means asking for the
descriptor of a negation over a not-yet-known parameter, which throws. Since the
optimization only ever applies to dtype_text, the dtype_text check was moved
before the descriptor is requested, so the reference operand is only inspected
when the literal is actually a string.

Relative date/time verbs were converted at prepare time (core_0053, core_1173, core_5421).

'NOW', 'TODAY', 'TOMORROW' and 'YESTERDAY' are resolved at execution against
the current clock. Converting them during prepare both failed and produced a value
frozen at prepare time. MAKE_constant_from_literal() now detects these four verbs
(case-insensitive, surrounding blanks ignored) and declines to convert when the target
is a date/time type, leaving them to the runtime.

Testing

test_correctness.sql

Correctness. A dedicated script compares the optimized form against the canonical
one — col <op> 'literal' vs col <op> CAST('literal' AS <type>) — over 100-row
deterministic data sets: 47 assertions covering >=, =, <>, literal-on-the-left,
BETWEEN and NOT BETWEEN, across DATE, TIME, TIME WITH TIME ZONE,
TIMESTAMP, TIMESTAMP WITH TIME ZONE, DOUBLE, DECFLOAT(16), DECFLOAT(34),
INT128, plus the relative verbs and literal-vs-literal forms.

All 47 assertions pass, and produce identical values on a build with the
optimization disabled and on the patched build — the change does not alter results,
only where the conversion happens.

Regression suite. core_5395, core_0053, core_1173 and core_5421 pass.

Performance

perf_compare.sql

2 000 000 rows per table, warmed page cache, full scan, COUNT(*) over a range, isql with SET STATS ON. Two builds, identical except for this change, run against the same script and the same data:

Type Without the change With the change Speed-up
DATE 1.994 s 0.505 s 3.95×
TIMESTAMP 2.209 s 0.545 s 4.05×
DECFLOAT(16) 0.916 s 0.775 s 1.18×
INT128 0.709 s 0.617 s 1.15×

Row counts and fetch counts are identical between the two builds for every query.

Date/time parsing benefits most, numeric parsing least — as expected, since turning a
string into a timestamp costs far more than turning it into an integer.

BETWEEN upper bound (arg3), DATE, same data — converting the upper bound in
addition to the lower one is worth roughly a further 2.3× on this query.

Note for reviewers

Converting at prepare time moves the failure point for a malformed literal: something
like WHERE date_col >= 'not a date' would raise its conversion error while preparing
the statement rather than while executing it. This follows from the design and is the
intended trade-off, but it is a user-visible change in when the error appears, so it
is worth an explicit decision. I have not re-verified the exact error text and timing
on the current build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant