View on GitHub

Red

A WiP ORM for Raku

Optional RakuAST query compiler

The optional compiler translates supported query callbacks during CHECK. use Red exports red-compile. Use the RakuAST frontend and explicitly opt in for each compilation unit:

use Red:api<2>;
CHECK red-compile;

my $minimum = 10;
my $query = Product.^all
    .map({ .price + 2 })
    .grep({ $_ > $minimum })
    .map({ "price: $_" });

Run the program with raku -Ilib program.raku. Recent Rakudo versions use RakuAST by default; set RAKUDO_RAKUAST=1 when needed. This integration is experimental and tested with Rakudo 2026.09. Loading Red without opting in keeps the existing frontend and callback behavior.

The compiler recognizes literal blocks, pointy blocks (including tuple parameters), placeholder parameters, and single-Whatever expressions passed to map, grep, first, and sort. Supported expressions include model columns, constant indexes and Hash keys, dynamic JSON indexes and key existence, scalar and container projections, arithmetic, comparisons, string concatenation and interpolation, complete conditionals, and defined, contains, elems, starts-with, ends-with, substr, index, lc, uc, and cast. Membership predicates accept literal word lists, captured arrays and subqueries. Hash projections accept both fat arrows and colonpairs. Boolean operators are translated in predicates only. Comparisons retain the resolved Red operator, including its numeric or textual coercions and SQL casts.

Templates bind against the current projection, so SQL aliases and SELECT boundaries remain the responsibility of the existing query planner. Scalar lexical captures are read when the query is constructed and retained if the planner needs to bind the template against another SELECT layer.

During CHECK the compiler specializes factories that construct Red::AST nodes directly, along with the required validators. Runtime query construction invokes these factories rather than interpreting the expression plan. Factories are shared across calls; captured values and SQL subjects remain private to each query. CHECK time is separate from runtime query construction and execution.

Map templates also record column and JSON assignments, arithmetic compound assignments (+=, -=, *=, /=, %= and ~=), increments, decrements and JSON deletion. These effects use the existing update pipeline and are persisted with .save. Updates are buffered until the template evaluates successfully. Simple untyped scalar locals and sequential statements can be compiled too. Compound assignment to a projected scalar or a local variable changes the expression used by later statements; it does not turn that projection into a database update. Postfix increment/decrement retains its original return value.

Expressions independent of the query element, including constructors and function calls, can be bound as runtime captures. Their code runs during query construction, never during CHECK. Ordinary native receivers still execute their original callback.

Plain lexical helpers can be compiled into their callers, including nested helper calls with unconstrained positional parameters:

sub above-one($value) { $value > 1 }
my &predicate = -> $row { $row.price > 1 };

Product.^all.grep({ above-one(.price) });
Product.^all.grep(&predicate);

Named routine callbacks and lexical variables initialized with literal blocks or pointy blocks are supported too. Their bodies must use supported expressions and must not depend on captured variables from another closure frame. Helpers with recursion, constrained/default/named parameters, return annotations, traits, phasers or mutations retain symbolic evaluation. Named callable closure captures also retain that path, preserving their original lexical frame.

Runtime guards verify that a named callback still has the compiled body and that inlined routines have not been reassigned or wrapped. A changed callable returns to symbolic evaluation before effectful captures run. Repeated helper parameters reuse captured argument values; helper code is never executed during CHECK. These guards also survive module precompilation.

An unsupported callback is left on the existing symbolic evaluation path, including its existing fallback policy. Opaque/dynamic callbacks, untranslatable functions of the query element, loops, phasers, constrained signatures, custom operators and conditional updates are not compiled. Unsupported syntax is rejected as a complete callback before any updates are submitted. Translation never executes the original callback during CHECK.

Instrumented calls check their receiver at runtime. A ResultSeq receives an internal template alongside the original callback. Other receivers receive the original arguments, preserving native dispatch, adverbs, and writable results. The compiler does not add SQL semantics to ordinary Arrays or Seqs.

To warn whenever an executed Red callback falls back to symbolic evaluation:

CHECK red-compile(:warn);

Warnings include the operation, callback location, source and fallback reason. Unsupported syntax is diagnosed when that Red query is constructed, so native Array/Seq calls and callbacks that never run do not warn. Runtime template validation failures are reported too, once per query stage even if a SELECT boundary requires rebinding. The fallback still uses the original callback.

You can also enable warnings dynamically or through the environment:

my $*RED-COMPILER-WARN = True;
CHECK red-compile;
RED_COMPILER_WARN=1 RAKUDO_RAKUAST=1 raku -Ilib program.raku

Precedence is the explicit :warn/:!warn parameter, then a defined Boolean $*RED-COMPILER-WARN, then RED_COMPILER_WARN. An explicit False disables warnings even when the environment enables them. Environment values 1, true, yes and on enable warnings, ignoring case and surrounding whitespace; other values disable them. The default is silent.

The parameter applies to instrumented calls in the current compilation unit; dynamic and environment settings also cover uncompiled Red callbacks elsewhere. If warnings are enabled only at runtime, an uninstrumented callback reports No RakuAST template was supplied with its location, instead of the precise CHECK-time rejection reason. Set the environment before compilation or use :warn to retain those rejection details. SQL plan-cache misses/bypasses are not compilation failures, and database errors retain their normal exception behavior.

For compilation diagnostics, pass a reporting callback:

CHECK red-compile(:report(-> :$method, :$source, :$status, :$reason {
    note "$method: $status ($reason)";
}));

Reports distinguish translated callbacks from unsupported syntax and include the original callback source. A compiled template can still be inapplicable to a particular runtime projection. To observe successful template evaluation, set $*RED-COMPILER-REPORT to a callable; it receives :status<used>, :plan, and :engine<direct> for generated factories, or :status<fallback> and :reason. Manually constructed templates without a factory report :engine<interpreted>. This is useful for tests that must distinguish compilation from symbolic fallback.

To measure factory runtime separately from CHECK, database execution, and the query planner, run raku -Ilib tools/benchmark-compiler.raku. The benchmark emits JSON samples comparing interpreted and direct factories in the same process, with warmup and alternating measurement order. Factory specialization happens before timing. --iterations=1000 and --rounds=7 control the sample size. These measurements isolate binding and AST construction; they do not represent the speedup of an entire query.

Runtime query-plan cache

A ResultSeq automatically reuses its SQL plan when its state can be fingerprinted safely. Opaque mutable values bypass caching. The database is still queried each time, using a fresh statement and cursor. Changes to the query, driver, pagination or SQL metadata select a different plan. Diagnostic comments are attached for the current execution. .cache continues to materialize query results; it is separate from this plan cache.

The optional compiler also shares predicate trees for direct comparisons between a model field and one captured scalar (Int, Str, Rat or Num). Each query reads its capture once during construction and retains that private snapshot:

sub expensive($minimum) {
    Product.^all.grep({ .price > $minimum })
}
my $first = expensive(10);
my $second = expensive(20);

After warming up, these calls reuse the predicate factory output and SQL while keeping their parameters independent. Other callbacks, mutable captures, NULL captures and updates retain their existing factory/fallback behavior. Comparisons requiring scalar coercion also use ordinary factories. Caches are bounded to 128 entries, synchronized, and live only in the current process. Prepared statements are not shared between connections or iterators. The SELECT wrapper and current metadata are still inspected; reusing a predicate does not eliminate every AST allocation. Warm-up has a cost, and for small predicates the construction cost can remain similar. Plan reuse still hydrates every row, so end-to-end gains depend on how much of the workload is spent translating SQL. Drivers based on CommonSQL enable plan caching; other drivers retain their existing prepare path unless they opt in with supports-query-plans.

$*RED-COMPILER-REPORT additionally receives filter-hit, filter-miss, plan-hit, plan-miss and plan-fallback events. Plan fallback includes a reason. For comparisons, my $*RED-QUERY-PLAN-CACHE = False disables both runtime caches. Run raku -Ilib tools/benchmark-query-plans.raku to compare construction, translation/planning (dry run), and execution with caching enabled and disabled. The benchmark alternates variants and reports cold-plan and warmed measurements in microseconds per operation as JSON. Module compilation and database setup are excluded. A separate prototype-samples series compares first specialization with the saved symbolic AST against the runtime factory, using empty predicate caches in both variants. For direct field/scalar comparisons, CHECK also constructs a Red::AST prototype with symbolic field and parameter slots. That object is embedded in the template and saved with the module in .precomp. Its first use resolves the model column, alias and parameter type from the prototype; later calls reuse the resolved predicate. Runtime captures, connections, query objects and SQL plans are never saved in the module. Other expressions continue to use CHECK-generated factories; this does not eliminate the runtime SELECT envelope or every AST allocation.

Measuring query construction and cache bookkeeping

raku -Ilib tools/benchmark-query-construction.raku measures filters, arithmetic projections and chained projections. It checks query results before timing construction, warms the tasks, and alternates their order. Set RAKUDO_RAKUAST=1 to include the optional CHECK compiler. --iterations and --rounds control the sample size.

raku -Ilib tools/benchmark-query-cache.raku isolates cache lookup costs with 1, 16 and 128 entries. It measures both a warmed working set and continual eviction, checks builder counts, and reports microseconds per lookup. --iterations and --rounds control the sample size. These cache and field-access measurements isolate individual operations; use the query-plan benchmark to evaluate their effect on SQL workloads.