From 94135d7c20d76a3d450012236f89937ca4996850 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 15:45:01 +0900 Subject: [PATCH 1/9] feat(columnar): seal standalone projection inputs Expose generic seal_input with a compatible seal_join_input spelling. Admit complete direct and narrowly filtered standalone readers without a JOIN prerequisite, preserving mutation freeze, output floors and typed errors. Refs #1372. --- CHANGELOG.md | 5 +++ abi/libwirelog-1.0.abi.json | 1 + abi/libwirelog-1.0.macos.symbols | 1 + abi/libwirelog-1.0.symbols | 1 + abi/libwirelog-1.0.windows.symbols | 1 + docs/MEMORY.md | 24 ++++++++------ docs/SEMANTICS.md | 8 +++-- tests/test_session_cold_relation.c | 46 ++++++++++++++++++++++++++ tests/test_wirelog_advanced_spill.c | 51 +++++++++++++++++++++++++++-- wirelog/columnar/internal.h | 9 +++-- wirelog/columnar/session.c | 8 +++++ wirelog/wirelog-advanced.h | 21 ++++++++---- wirelog/wirelog_advanced.c | 11 +++++-- 13 files changed, 160 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e472cb2..2c1e7e610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to wirelog are documented in this file. ### Added +- **Standalone sealed-input reads** (#1372): `wirelog_session_seal_input` + permits supported direct/filtered projections without a JOIN consumer. + The existing `wirelog_session_seal_join_input` spelling remains compatible; + mutation freeze, evaluation closure and resident output floors are unchanged. + - **Filtered sealed-input projections** (#1372): bounded column projections can apply one signed integer column/literal comparison while preserving resident output floors, exact results, failure recovery and fixed-budget reclamation. diff --git a/abi/libwirelog-1.0.abi.json b/abi/libwirelog-1.0.abi.json index 90fb28b19..612a659d9 100644 --- a/abi/libwirelog-1.0.abi.json +++ b/abi/libwirelog-1.0.abi.json @@ -91,6 +91,7 @@ + diff --git a/abi/libwirelog-1.0.macos.symbols b/abi/libwirelog-1.0.macos.symbols index 4713c57d0..16cbf3b3b 100644 --- a/abi/libwirelog-1.0.macos.symbols +++ b/abi/libwirelog-1.0.macos.symbols @@ -66,6 +66,7 @@ wirelog_session_make_compound wirelog_session_make_compound_typed wirelog_session_remove wirelog_session_remove_typed +wirelog_session_seal_input wirelog_session_seal_join_input wirelog_session_set_delta_cb wirelog_session_set_typed_delta_cb diff --git a/abi/libwirelog-1.0.symbols b/abi/libwirelog-1.0.symbols index 9bb4007ac..4f0bc2fdb 100644 --- a/abi/libwirelog-1.0.symbols +++ b/abi/libwirelog-1.0.symbols @@ -89,6 +89,7 @@ wirelog_session_make_compound wirelog_session_make_compound_typed wirelog_session_remove wirelog_session_remove_typed +wirelog_session_seal_input wirelog_session_seal_join_input wirelog_session_set_delta_cb wirelog_session_set_typed_delta_cb diff --git a/abi/libwirelog-1.0.windows.symbols b/abi/libwirelog-1.0.windows.symbols index 4713c57d0..16cbf3b3b 100644 --- a/abi/libwirelog-1.0.windows.symbols +++ b/abi/libwirelog-1.0.windows.symbols @@ -66,6 +66,7 @@ wirelog_session_make_compound wirelog_session_make_compound_typed wirelog_session_remove wirelog_session_remove_typed +wirelog_session_seal_input wirelog_session_seal_join_input wirelog_session_set_delta_cb wirelog_session_set_typed_delta_cb diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 9c47d83b3..ed76bb7e3 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -4724,8 +4724,8 @@ persistent trace storage cold, nor add differential cold execution. ### Persistent sealed numeric JOIN input (#1372) -The internal `wl_columnar_session_seal_join_input` operation can now convert one -right-only numeric EDB into a session-owned immutable cold binding before the +The internal `wl_columnar_session_seal_input` operation converts one eligible +numeric EDB into a session-owned immutable cold binding before the first STEP or SNAPSHOT attempt. It captures privately, checks exclusive resident retirement, refunds the original allocation, removes the raw registry entry, and publishes the binding without a fallible gap. Failed capture, cancellation, @@ -4737,15 +4737,15 @@ cannot survive this transition. The supported plan is serial, nonrecursive, and contains only audited VARIABLE/JOIN/MAP/CONSOLIDATE operations, plus the restricted projection FILTER -described below. The cold name must have a JOIN right consumer and may also feed -the exact direct/filtered projection body below; it cannot be a relation head +described below. The cold name must have an eligible consumer: a JOIN right +input or the exact direct/filtered projection body below; it cannot be a relation head or an arbitrary VARIABLE consumer. Eligible JOINs use one INT64 key, scalar numeric rows, no timestamps and no right filter. Multiple eligible JOINs may consume the same binding. Spill batch/index caps must be positive. Opaque rewrites, workers, controlled execution, delta callbacks and competing JOIN batch modes are refused. The plan must remain immutable for the session lifetime, -and session operations remain externally serialized. This is an internal -opt-in, not a new installed API or automatic eviction policy. +and session operations remain externally serialized. The advanced public opt-in +is described below; this is not an automatic eviction policy. Successful sealing makes all fact and compound mutation unsupported. Generic fact/file loaders reject before staging or opening adapters, without poisoning @@ -4811,8 +4811,10 @@ fixed 256 MiB allowance and refuse a 4096-by-4096 JOIN's resident output with retained-source control remains the causal reclamation proof; this public test qualifies the actual host boundary and output floor. -After loading all inputs, call `wirelog_session_seal_join_input` for one eligible -numeric JOIN input. It uses the exact immutable plan/mode restrictions above; +After loading all inputs, call `wirelog_session_seal_input` for one eligible +numeric input. `wirelog_session_seal_join_input` remains a compatibility spelling +with identical eligibility, errors and lifetime. A projection-only program can +seal its source without a JOIN. It uses the exact immutable plan/mode restrictions above; opaque optimized plans, VARIABLE reads outside the direct-projection subset, recursive, worker, delta and controlled paths remain ineligible. The public call is synchronous and has no cancellation callback. Success freezes all fact/compound mutation for the @@ -4891,8 +4893,10 @@ actual corresponding CI run, not a Linux macro compile. A sealed numeric JOIN input can also supply an exact three-operator `VARIABLE(cold) → MAP(columns) → CONSOLIDATE` rule. For example, alongside `result(k,x,y + 0) :- a(k,x), b(k,y).`, a declared `keys(k)` head can use -`keys(k) :- b(k,v).`. At least one eligible right JOIN consumer must remain; -scan-only programs still cannot use `wirelog_session_seal_join_input`. +`keys(k) :- b(k,v).`. Such a projection may also be the source's only consumer: +`wirelog_session_seal_input` supports direct and narrowly filtered standalone +reads. The compatibility `wirelog_session_seal_join_input` spelling does too. +A known but unused source or a source with unsupported consumers is refused. Seal preflight and ordinary dispatch share the same full-body predicate. It accepts valid reordered/repeated column indices and canonical column-reference diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index 8472eb95a..4108219b9 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -250,13 +250,15 @@ caps. NULL preserves legacy defaults; existing creators are unchanged. Inline facts still load before return. Options and directory storage are borrowed only for construction; the program stays borrowed for the session lifetime. -`wirelog_session_seal_join_input` exposes the internal pre-evaluation -numeric JOIN-input transition. A sealed JOIN input may additionally feed an exact +`wirelog_session_seal_input` exposes the internal pre-evaluation numeric-input +transition; `wirelog_session_seal_join_input` is a compatibility spelling with +identical behavior. A sealed JOIN input may additionally feed an exact `VARIABLE → MAP → CONSOLIDATE` direct column-projection rule. Column indices may repeat or reorder. One FILTER between VARIABLE and MAP may compare one INT64 column with one integer literal (`=`, `!=`, `<`, `>`, `<=`, `>=`), with the column on the left. Other predicates, arbitrary expressions and cold left JOINs remain -unsupported, and at least one eligible right JOIN consumer is required. Reads +unsupported. An eligible direct/filtered projection alone is sufficient; a +JOIN consumer is not required. Reads use independent bounded pins; projected output and consolidation remain resident. Output capacity is admitted for the full source row count even if no row passes. It retains the conservative serial immutable-plan diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 5bba92a7d..8c139481c 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -1217,6 +1217,51 @@ static void filtered_shape(void) destroy(&f); } +static void standalone_reads(void) +{ + for (unsigned filtered = 0; filtered < 2; filtered++) { + char text[256]; + snprintf(text, sizeof(text), + ".decl b(k:int64,v:int64)\n.decl keys(k:int64)\n" + "keys(k) :- b(k,v)%s.\n", filtered ? ", v >= 164" : ""); + fixture_t f = fixture(text, 1024*1024, 8*1024*1024); + for (unsigned i = 0; i < 8192; i++) { + int64_t r[] = {(int64_t)i, 100+(int64_t)i}; + CHECK(wl_session_insert(f.session, "b", r, 1, 2) == 0); + } + CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, NULL) == OK); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(s->cold_relation && !session_find_rel(s, "b")); + const wl_plan_relation_t *rp = projection_plan(&f); + wl_plan_relation_t wide = *rp; + wl_plan_op_t ops[4]; memcpy(ops, rp->ops, rp->op_count*sizeof(*ops)); + wide.ops = ops; + wl_plan_op_t *map = &ops[rp->op_count-2]; + uint32_t indices[64] = {0}; + map->project_indices = indices; map->project_count = 64; + map->map_exprs = NULL; map->map_expr_count = 0; + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_ops_cold_projection(&wide, &stack, s) == ENOSPC); + CHECK(!stack.top && reserved(&f) == before); + projection_events_t events = {.rows = 8192, + .first = filtered ? 64u : 0u}; + read_fault = 5; + CHECK(wl_session_snapshot(f.session, projection_observe, + &events) == EIO); + CHECK(!events.keys && !events.joined && s->cold_relation); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, + NULL) == WL_COLUMNAR_SPILL_BUSY); + CHECK(wl_session_snapshot(f.session, projection_observe, &events) == 0); + CHECK(events.keys == 8192-events.first && !events.joined); + CHECK(events.sum == UINT64_C(8192)*8191/2 - + (uint64_t)events.first*(events.first ? events.first-1 : 0)/2); + destroy(&f); + } +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -1226,6 +1271,7 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + standalone_reads(); projection_shape(); filtered_predicates(); filtered_shape(); for (unsigned filtered = 0; filtered < 2; filtered++) { projection_consumers(filtered != 0); projection_failures(filtered != 0); diff --git a/tests/test_wirelog_advanced_spill.c b/tests/test_wirelog_advanced_spill.c index aec5a9bf3..57c250249 100644 --- a/tests/test_wirelog_advanced_spill.c +++ b/tests/test_wirelog_advanced_spill.c @@ -248,7 +248,7 @@ static void descriptors(void) static void refusals(void) { const char *scan = - ".decl b(k:int64,v:int64)\n.decl result(k:int64,v:int64)\nresult(k,v) :- b(k,v).\n"; + ".decl b(k:int64,v:int64)\n.decl result(k:int64,v:int64)\nresult(k,v + 1) :- b(k,v).\n"; for (unsigned kind = 0; kind < 8; kind++) { wirelog_program_t *p = parse(kind == 5 ? scan : query); wirelog_session_options_v1_t o = configured(); @@ -552,6 +552,52 @@ static void unsupported_filters(void) } } +static void standalone_reads(void) +{ + CHECK(wirelog_session_seal_input(NULL, "b") == WIRELOG_ERR_EXEC); + for (unsigned filtered = 0; filtered < 2; filtered++) + for (unsigned empty = 0; empty < 2; empty++) + for (unsigned mode = 0; mode < 3; mode++) { + char text[256]; + snprintf(text, sizeof(text), + ".decl b(k:int64,v:int64)\n.decl keys(k:int64)\n" + "keys(k) :- b(k,v)%s.\n", filtered ? ", v >= 164" : ""); + wirelog_program_t *p = parse(text); wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, + WIRELOG_BACKEND_DEFAULT, + 1, &o, &s) == WIRELOG_OK); + for (unsigned i = 0; i < (empty ? 1u : 128u); i++) { + int64_t row[] = {(int64_t)i, 100+(int64_t)i}; + CHECK(wirelog_session_insert(s, "b", row, 1, + 2) == WIRELOG_OK); + if (empty) CHECK(wirelog_session_remove(s, "b", row, 1, + 2) == WIRELOG_OK); + } + if (mode) { + CHECK((mode == 1 ? wirelog_session_seal_input(s, "b") : + wirelog_session_seal_join_input(s, "b")) == WIRELOG_OK); + check_mutation(s); + CHECK(wirelog_session_seal_input(s, + "b") == WIRELOG_ERR_EXEC); + } + if (mode == 2) CHECK(wirelog_session_step(s) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + mixed_events_t e = {0}; + CHECK(wirelog_session_snapshot(s, mixed_observe, + &e) == WIRELOG_OK); + unsigned count = empty ? 0u : filtered ? 64u : 128u; + CHECK(e.keys == count && !e.joined); + CHECK(e.key_bits[0] == (count == 128 ? UINT64_MAX : 0)); + CHECK(e.key_bits[1] == (count ? UINT64_MAX : 0)); + } + /* Every evaluation attempt closes the sealing window. */ + CHECK(wirelog_session_seal_input(s, "b") == WIRELOG_ERR_EXEC); + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + int main(void) { /* Issue #2287: a session created with a nonzero WIRELOG_JOIN_BATCH_BYTES @@ -571,7 +617,8 @@ int main(void) CHECK(mkdtemp(directory)); #endif descriptors(); lifecycle(); refusals(); typed_projection(); inline_facts(); - output_floor(); mixed_projection(); unsupported_filters(); + standalone_reads(); output_floor(); mixed_projection(); + unsupported_filters(); join_batch_knob(); #ifdef _WIN32 CHECK(_rmdir(directory) == 0); diff --git a/wirelog/columnar/internal.h b/wirelog/columnar/internal.h index fa61f39cc..594b9f8a4 100644 --- a/wirelog/columnar/internal.h +++ b/wirelog/columnar/internal.h @@ -4934,14 +4934,17 @@ int session_remove_rel(wl_col_session_t *sess, const char *name); /* Explicit internal pre-evaluation opt-in, under external session lifecycle * serialization. Caller keeps the plan immutable through session destruction. - * One canonical numeric EDB used only by supported ordinary right JOINs may be - * sealed. Success removes its resident registry owner and makes ALL fact and + * One canonical numeric EDB used only by supported right JOINs or complete + * direct/filtered projections may be sealed. Success removes its resident registry owner and makes ALL fact and * compound mutation unsupported, including zero-row requests. Failed sealing - * preserves the source/registry and refunds private work. No public host API. + * preserves the source/registry and refunds private work. * No readers may escape synchronous operations; callbacks must not reenter * source/session/store/governor lifecycle. STEP/SNAPSHOT recheck mode guards. * Original logical identity/view/ordinals survive in source_snapshot; storage * generation advances once. Old resident caches are not reusable. */ +wl_columnar_spill_status_t wl_columnar_session_seal_input( + wl_session_t *session, const char *name, wl_columnar_spill_cancel_fn cancel, + void *context); wl_columnar_spill_status_t wl_columnar_session_seal_join_input( wl_session_t *session, const char *name, wl_columnar_spill_cancel_fn cancel, void *context); diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 8f0bc43da..008dc9e44 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -629,6 +629,7 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, return false; bool projection = wl_columnar_eval_plan_cold_projection(rp, name, ncols, types); + if (projection) consumer = true; for (uint32_t oi = 0; oi < rp->op_count; oi++) { const wl_plan_op_t *op = &rp->ops[oi]; /* Reject all opaque rewrites, even if top-level names hide @@ -826,6 +827,13 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, return WL_COLUMNAR_SPILL_OK; } +wl_columnar_spill_status_t +wl_columnar_session_seal_input(wl_session_t *session, const char *name, + wl_columnar_spill_cancel_fn cancel, void *context) +{ + return wl_columnar_session_seal_join_input(session, name, cancel, context); +} + static uint32_t session_compound_max_epochs_from_env(void) { diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index 40d818ae5..647c3f503 100644 --- a/wirelog/wirelog-advanced.h +++ b/wirelog/wirelog-advanced.h @@ -182,8 +182,9 @@ wirelog_session_create_with_snapshot(wirelog_program_t *program, * turns it on). Any other value, including an empty one, "0", "0x10", "5x", * "abc" or one out of range, leaves it off. When it is on, that session * selects no spill JOIN even with positive join caps, and - * wirelog_session_seal_join_input() returns WIRELOG_ERR_EXEC. Changing the - * environment after creation does not affect an existing session. + * wirelog_session_seal_input() and its compatibility spelling return + * WIRELOG_ERR_EXEC. Changing the environment after creation does not affect + * an existing session. */ typedef struct { uint32_t struct_size; @@ -213,18 +214,18 @@ wirelog_session_create_with_options(wirelog_program_t *program, const wirelog_session_options_v1_t *options, wirelog_session_t **out); /** - * Seal one numeric JOIN input as persistent immutable spill input before + * Seal one numeric input as persistent immutable spill input before * the first STEP/SNAPSHOT attempt. Load all inputs first. Requires configured * spill JOIN caps and a serial nonrecursive plan using only audited * VARIABLE/JOIN/MAP/CONSOLIDATE operations and the restricted FILTER below. - * The selected name must have a JOIN - * right consumer with one INT64 key. It may also feed a complete + * The selected name must have an eligible consumer: a JOIN right input with + * one INT64 key or a complete * VARIABLE/MAP/CONSOLIDATE rule selecting only columns, optionally with one * FILTER before MAP comparing an INT64 column with an integer literal * (=, !=, <, >, <=, >=; column on the left). Projected results and consolidation * remain resident; output capacity is admitted for the source row count even - * when no row passes. Scan-only sealing and general cold pipelines - * are unsupported. No right filter, timestamps, references, + * when no row passes. Projection-only programs need no JOIN consumer. + * General cold pipelines remain unsupported. No right filter, timestamps, references, * opaque/materialized rewrites, worker/control/delta modes or delta callback. * Optimized plans may therefore be ineligible. The session must also not have * been created with WIRELOG_JOIN_BATCH_BYTES on (see @@ -246,6 +247,12 @@ wirelog_session_create_with_options(wirelog_program_t *program, * busy or exhausted-generation states. Error classification is per operation. */ WIRELOG_API wirelog_error_t +wirelog_session_seal_input(wirelog_session_t *session, + const char *relation); + +/** Compatibility spelling of wirelog_session_seal_input, with identical + * eligibility, errors and lifetime. A JOIN consumer is not required. */ +WIRELOG_API wirelog_error_t wirelog_session_seal_join_input(wirelog_session_t *session, const char *relation); diff --git a/wirelog/wirelog_advanced.c b/wirelog/wirelog_advanced.c index 38fdc155a..3c2284c16 100644 --- a/wirelog/wirelog_advanced.c +++ b/wirelog/wirelog_advanced.c @@ -561,12 +561,12 @@ wirelog_session_create_with_options(wirelog_program_t *program, } wirelog_error_t -wirelog_session_seal_join_input(wirelog_session_t *session, +wirelog_session_seal_input(wirelog_session_t *session, const char *relation) { if (!session || !relation) return WIRELOG_ERR_EXEC; wl_columnar_session_budget_denial_clear(COL_SESSION(session->inner)); - wl_columnar_spill_status_t status = wl_columnar_session_seal_join_input( + wl_columnar_spill_status_t status = wl_columnar_session_seal_input( session->inner, relation, NULL, NULL); switch (status) { case WL_COLUMNAR_SPILL_OK: return WIRELOG_OK; @@ -579,6 +579,13 @@ wirelog_session_seal_join_input(wirelog_session_t *session, } } +wirelog_error_t +wirelog_session_seal_join_input(wirelog_session_t *session, + const char *relation) +{ + return wirelog_session_seal_input(session, relation); +} + void wirelog_session_destroy(wirelog_session_t *session) { From 46737f0272b10b3cbaa02cbef89a1417cde7a0ed Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 16:11:34 +0900 Subject: [PATCH 2/9] feat(columnar): seal multiple independent input bindings --- CHANGELOG.md | 5 + docs/MEMORY.md | 14 ++ docs/SEMANTICS.md | 5 +- tests/test_session_cold_relation.c | 347 ++++++++++++++++++++++++++++ tests/test_wirelog_advanced_spill.c | 88 ++++++- wirelog/columnar/eval_plan.c | 13 +- wirelog/columnar/internal.h | 11 +- wirelog/columnar/join.c | 6 +- wirelog/columnar/ops.c | 15 +- wirelog/columnar/session.c | 111 +++++++-- wirelog/wirelog-advanced.h | 8 +- 11 files changed, 579 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1e7e610..a27be9e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to wirelog are documented in this file. ### Added +- **Multiple sealed inputs** (#1372): independently seal already-loaded numeric + sources in either order, retaining earlier bindings on a later failure. + Per-source lookup, destruction and narrow projection schema auditing preserve + exact results across resident/sealed mixtures and multiple JOIN consumers. + - **Standalone sealed-input reads** (#1372): `wirelog_session_seal_input` permits supported direct/filtered projections without a JOIN consumer. The existing `wirelog_session_seal_join_input` spelling remains compatible; diff --git a/docs/MEMORY.md b/docs/MEMORY.md index ed76bb7e3..b4d161826 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -4735,6 +4735,20 @@ source identity, view and row ordinals while advancing the storage generation. Generation overflow is refused before capture. Existing resident arrangements cannot survive this transition. +Several already-loaded inputs can be sealed independently in either order, +using one stable governed node per input. All facts must be loaded before the +first seal freezes mutation; additional sealing is allowed until evaluation. +Each capture commits only its own binding. Failed later captures preserve all +earlier bindings and their disk extents, and preserve the failed resident source. +Duplicate sealing is refused. Destruction releases every node and payload before +the session store. Raw lookup of any known cold name remains an explicit error. + +Complete direct/narrowly filtered projection rules over another loaded numeric +EDB can coexist whether that source is resident or already sealed. Auditing uses +that source's complete numeric schema and metadata, not the selected candidate's +width/types. This does not admit arbitrary resident filters or filters on IDBs, +and another source's valid projection cannot authorize an unused candidate. + The supported plan is serial, nonrecursive, and contains only audited VARIABLE/JOIN/MAP/CONSOLIDATE operations, plus the restricted projection FILTER described below. The cold name must have an eligible consumer: a JOIN right diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index 4108219b9..9f7fa8265 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -263,7 +263,10 @@ use independent bounded pins; projected output and consolidation remain resident Output capacity is admitted for the full source row count even if no row passes. It retains the conservative serial immutable-plan eligibility checks and makes all fact/compound mutation unsupported after -success. Refusal preserves the source but cannot reopen the seal window after +success. Additional already-loaded eligible inputs may be sealed independently +before evaluation; failure preserves prior bindings and the failed resident +source. Complete narrow projections over other resident numeric EDBs coexist +using their own schema checks. Refusal cannot reopen the seal window after an evaluation attempt. Cold lookup never substitutes empty input for an unsupported consumer. Every session operation remains externally serialized. Memory admission, allocation and disk capacity/quota failures retain distinct diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 8c139481c..1aa48c833 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -1262,6 +1262,349 @@ static void standalone_reads(void) } } +static const char *multiple_query = + ".decl a(k:int64,v:int64)\n.decl b(k:int64,x:int64,v:int64)\n" + ".decl c(k:int64,v:int64)\n.decl unused(k:int64)\n" + ".decl pa(k:int64)\n.decl pb(k:int64)\n.decl pc(k:int64)\n" + "pa(k) :- a(k,v), v >= 132.\npb(k) :- b(k,x,v), v >= 216.\n" + "pc(k) :- c(k,v).\n"; +static fixture_t multiple_fixture(uint64_t quota) +{ + fixture_t f = fixture(multiple_query, 4*1024*1024, quota); + int64_t unused = 0; + CHECK(wl_session_insert(f.session, "unused", &unused, 1, 1) == 0); + for (int64_t i = 0; i < 64; i++) { + int64_t a[] = {i, 100+i}, b[] = {i, -i, 200+i}, c[] = {i, 300+i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "c", c, 1, 2) == 0); + } + return f; +} +typedef struct { uint64_t bits[3]; } multiple_events_t; +static void multiple_observe(const char *name, const int64_t *row, + uint32_t width, + void *context) +{ + multiple_events_t *e = context; + unsigned index = !strcmp(name, "pa") ? 0u : !strcmp(name, "pb") ? 1u : 2u; + CHECK(index != 2 || !strcmp(name, "pc")); + CHECK(width == 1 && row[0] >= 0 && row[0] < 64); + uint64_t bit = UINT64_C(1) << (unsigned)row[0]; + CHECK(!(e->bits[index] & bit)); e->bits[index] |= bit; +} +static void multiple_result(fixture_t *f) +{ + multiple_events_t e = {0}; + CHECK(wl_session_snapshot(f->session, multiple_observe, &e) == 0); + CHECK(e.bits[0] == (UINT64_MAX << 32) && + e.bits[1] == (UINT64_MAX << 16) && e.bits[2] == UINT64_MAX); +} +static void multiple_bindings(void) +{ + const char *orders[][3] = {{"a", "b", "c"}, {"c", "b", "a"}, + {"b", "a", "c"}}; + for (unsigned order = 0; order < 3; order++) + for (unsigned count = 0; count < 4; count++) { + fixture_t f = multiple_fixture(1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + for (unsigned i = 0; i < count; i++) { + CHECK(wl_columnar_session_seal_input(f.session, + orders[order][i], NULL, NULL) == OK); + CHECK(wl_columnar_session_cold_find(s, orders[order][i])); + CHECK(!session_find_rel(s, orders[order][i])); + CHECK(wl_columnar_session_seal_input(f.session, + orders[order][i], NULL, NULL) == WL_COLUMNAR_SPILL_BUSY); + } + CHECK(wl_columnar_session_seal_input(f.session, "unused", NULL, + NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + if (wl_columnar_session_cold_find(s, "c")) + CHECK(s->last_inserted_relation == + wl_columnar_session_cold_find(s, "c")->name); + for (unsigned i = 0; i < count; i++) { + wl_plan_op_t op = {.op = WL_PLAN_OP_VARIABLE, + .relation_name = orders[order][i]}; + eval_stack_t stack; eval_stack_init(&stack); + CHECK(col_op_variable(&op, &stack, s) == ENOTSUP && !stack.top); + } + if (count == 3) { + multiple_events_t e = {0}; read_fault = 5; + CHECK(wl_session_snapshot(f.session, multiple_observe, + &e) == EIO); + CHECK(!e.bits[0] && !e.bits[1] && !e.bits[2]); + } else CHECK(wl_session_step(f.session) == 0); + multiple_result(&f); multiple_result(&f); + CHECK(wl_columnar_session_seal_input(f.session, "c", NULL, + NULL) == WL_COLUMNAR_SPILL_BUSY); + destroy(&f); + } + for (unsigned retain = 0; retain < 2; retain++) { + fixture_t f = multiple_fixture(1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f); + wl_columnar_session_test_retain_cold_source = retain != 0; + for (unsigned i = 0; i < 3; i++) + CHECK(wl_columnar_session_seal_input(f.session, orders[0][i], NULL, + NULL) == OK); + wl_columnar_session_test_retain_cold_source = false; + CHECK(retain ? reserved(&f) > before : reserved(&f) < before); + unsigned nodes = 0; + for (wl_columnar_session_cold_relation_t *c = s->cold_relation; c; + c = c->next){ + CHECK((c->retained_source != NULL) == (retain != 0)); nodes++; + } + CHECK(nodes == 3 && + wl_columnar_session_test_has_retained_cold_source(s) == + (retain != 0)); + multiple_result(&f); destroy(&f); + CHECK(!wl_columnar_session_test_has_retained_cold_source(NULL)); + } +} +static void multiple_seal_failures(void) +{ + for (unsigned later = 0; later < 2; later++) + for (unsigned kind = 0; kind < 5; kind++) { + fixture_t f = multiple_fixture(1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == OK); + wl_columnar_session_cold_relation_t *first = + wl_columnar_session_cold_find(s, "a"); + if (later) CHECK(wl_columnar_session_seal_input(f.session, "b", + NULL, NULL) == OK); + const char *failed = later ? "c" : "b"; + wl_columnar_session_cold_relation_t *second = + wl_columnar_session_cold_find(s, "b"); + col_rel_t *source = session_find_rel(s, failed); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + cancellation_t cancellation = {.stop = 3}; + wl_columnar_source_access_reader_t lease = {0}; + wl_columnar_spill_status_t expected = WL_COLUMNAR_SPILL_IO; + if (kind == 0) write_fault = 1; + if (kind == 1) { + allocation_fault = 1; expected = WL_COLUMNAR_SPILL_ALLOCATION; + } + if (kind == 2) expected = WL_COLUMNAR_SPILL_CANCELLED; + if (kind == 3) { + CHECK(col_rel_source_reader_acquire(source, &lease) == 0); + expected = WL_COLUMNAR_SPILL_BUSY; + } + wl_plan_expr_buffer_t saved = {0}; wl_plan_op_t *filter = NULL; + if (kind == 4) { + for (uint32_t i = 0; i < f.plan->stratum_count; i++) + for (uint32_t j = 0; j < f.plan->strata[i].relation_count; + j++) { + const wl_plan_relation_t *rp = + &f.plan->strata[i].relations[j]; + if (!strcmp(rp->name, + "pa")) filter = (wl_plan_op_t *)&rp->ops[1]; + } + CHECK(filter && filter->op == WL_PLAN_OP_FILTER); + saved = filter->filter_expr; filter->filter_expr.data = NULL; + expected = WL_COLUMNAR_SPILL_UNSUPPORTED; + } + CHECK(wl_columnar_session_seal_input(f.session, failed, + kind == 2 ? cancel : NULL, + &cancellation) == expected); + if (kind == 3) CHECK(col_rel_source_reader_release(&lease) == 0); + if (filter) filter->filter_expr = saved; + CHECK(wl_columnar_session_cold_find(s, + "a") == first && !first->next); + CHECK(session_find_rel(s, + failed) == source && !wl_columnar_session_cold_find(s, failed)); + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + CHECK(wl_columnar_session_cold_find(s, "b") == second); + CHECK(wl_columnar_session_seal_input(f.session, failed, NULL, + NULL) == OK); + if (!later) CHECK(wl_columnar_session_seal_input(f.session, "c", + NULL, NULL) == OK); + multiple_result(&f); destroy(&f); + } + /* Combined persistent quota refuses a later capture, preserving a usable mixed state. */ + for (unsigned third = 0; third < 2; third++) { + fixture_t f = multiple_fixture(third ? 8000 : 4096); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + if (third) CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, + NULL) == OK); + const char *name = third ? "c" : "b"; + col_rel_t *source = session_find_rel(s, name); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + CHECK(wl_columnar_session_seal_input(f.session, name, NULL, + NULL) == WL_COLUMNAR_SPILL_QUOTA); + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + CHECK(session_find_rel(s, name) == source); + multiple_result(&f); + CHECK(wl_columnar_session_seal_input(f.session, name, NULL, + NULL) == WL_COLUMNAR_SPILL_BUSY); + destroy(&f); + } +} + +static void multiple_memory_floor(void) +{ + for (unsigned later = 0; later < 2; later++) { + fixture_t f = multiple_fixture(1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + if (later) CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, + NULL) == OK); + const char *name = later ? "c" : "b"; + col_rel_t *source = session_find_rel(s, name); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + uint64_t bytes = 4*1024*1024 - before - 64; + wl_columnar_memory_reservation_t token; + wl_columnar_memory_reservation_init(&token); + CHECK(wl_columnar_memory_reserve_checked( + wl_columnar_memory_governor_ref_get(f.governor), + bytes, &token) <= WL_COLUMNAR_MEMORY_ADMISSION_ADVISORY); + void *pressure = malloc((size_t)bytes); CHECK(pressure); + memset(pressure, 0, (size_t)bytes); + CHECK(wl_columnar_memory_commit(&token, pressure)); + CHECK(wl_columnar_session_seal_input(f.session, name, NULL, + NULL) == WL_COLUMNAR_SPILL_MEMORY); + CHECK(reserved(&f) == before+bytes && session_find_rel(s, + name) == source); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + free(pressure); CHECK(wl_columnar_memory_release(&token)); + CHECK(reserved(&f) == before); + CHECK(wl_columnar_session_seal_input(f.session, name, NULL, + NULL) == OK); + multiple_result(&f); destroy(&f); + } +} + +static void multiple_idb_filter_refusal(void) +{ + const char *text = ".decl a(k:int64,v:int64)\n.decl z(k:int64,v:int64)\n" + ".decl keys(k:int64)\nz(k,v) :- a(k,v).\nkeys(k) :- z(k,v), v > 0.\n"; + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + int64_t row[] = {1, 2}; + CHECK(wl_session_insert(f.session, "a", row, 1, 2) == 0); + uint64_t before = reserved(&f); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + CHECK(reserved(&f) == before && session_find_rel(COL_SESSION(f.session), + "a")); + destroy(&f); +} + +static void multiple_schema_audit(void) +{ + for (unsigned bad = 0; bad < 6; bad++) { + fixture_t f = multiple_fixture(1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + col_rel_t *b = session_find_rel(s, "b"); + wirelog_column_type_t *original = b->column_types; + wirelog_column_type_t types[] = {WIRELOG_TYPE_INT64, WIRELOG_TYPE_INT64, + WIRELOG_TYPE_INT64}; + unsigned slot = 0; while (slot < s->nrels && s->rels[slot] != b) slot++; + CHECK(slot < s->nrels); + uint64_t before = reserved(&f); + for (unsigned phase = 0; phase < 2; phase++) { + b->column_types = types; + if (bad == 0) types[2] = WIRELOG_TYPE_FLOAT; + if (bad == 1) types[1] = WIRELOG_TYPE_STRING; /* Nonprojected lane. */ + if (bad == 2) b->has_graph_column = true; + if (bad == 3) b->compound_count = 1; + if (bad == 4) b->ncols = 2; /* Predicate col2 must use B's width. */ + if (bad == 5) s->rels[slot] = NULL; + CHECK(wl_columnar_session_seal_input(f.session, phase ? "c" : "a", + NULL, NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + b->column_types = original; b->has_graph_column = false; + b->compound_count = 0; b->ncols = 3; s->rels[slot] = b; + CHECK(reserved(&f) == before && !wl_columnar_session_cold_find(s, + "c")); + if (!phase) { + CHECK(!s->cold_relation); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == OK); + before = reserved(&f); + } else CHECK(wl_columnar_session_cold_find(s, "a")); + } + CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, NULL) == OK); + CHECK(wl_columnar_session_seal_input(f.session, "c", NULL, NULL) == OK); + multiple_result(&f); destroy(&f); + } +} +typedef struct { unsigned seen[2]; } multiple_join_events_t; +static void multiple_join_observe(const char *name, const int64_t *row, + uint32_t n, + void *context) +{ + multiple_join_events_t *e = context; + unsigned which = !strcmp(name, "r") ? 0u : 1u; + CHECK(!which || !strcmp(name, "s")); + CHECK(n == 3 && row[0] >= 0 && row[0] < 4 && row[1] == 10+row[0]); + CHECK(row[2] == (which ? 200 : 100)+row[0]); + unsigned bit = 1u << (unsigned)row[0]; + CHECK(!(e->seen[which] & bit)); e->seen[which] |= bit; +} +static void multiple_right_joins(void) +{ + const char *text = + ".decl a(k:int64,v:int64)\n.decl b(k:int64,v:int64)\n.decl c(k:int64,v:int64)\n" + ".decl r(k:int64,x:int64,y:int64)\n.decl s(k:int64,x:int64,y:int64)\n" + "r(k,x,y + 0) :- a(k,x),b(k,y).\ns(k,x,y + 0) :- a(k,x),c(k,y).\n"; + for (unsigned reverse = 0; reverse < 2; reverse++) { + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + for (int64_t i = 0; i < 64; i++) { + int64_t a[] = {i, 10+i}, b[] = {i, 100+i}, c[] = {i, 200+i}; + if (i < 4) CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "c", c, 1, 2) == 0); + } + CHECK(wl_columnar_session_seal_input(f.session, reverse ? "c" : "b", + NULL, NULL) == OK); + CHECK(wl_columnar_session_seal_input(f.session, reverse ? "b" : "c", + NULL, NULL) == OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + multiple_join_events_t e = {0}; + CHECK(wl_session_snapshot(f.session, multiple_join_observe, + &e) == 0); + CHECK(e.seen[0] == 15 && e.seen[1] == 15); + if (!repeat) CHECK(COL_SESSION( + f.session)->spill_join_selected_count == 2); + } + destroy(&f); + } +} + +static void multiple_runtime_guard(void) +{ + fixture_t f = multiple_fixture(1024*1024); + const char *names[] = {"c", "a", "b"}; + for (unsigned i = 0; i < 3; i++) + CHECK(wl_columnar_session_seal_input(f.session, names[i], NULL, + NULL) == OK); + const wl_plan_relation_t *pb = NULL; wl_plan_op_t *map = NULL; + for (uint32_t i = 0; i < f.plan->stratum_count; i++) + for (uint32_t j = 0; j < f.plan->strata[i].relation_count; j++) { + const wl_plan_relation_t *rp = &f.plan->strata[i].relations[j]; + if (!strcmp(rp->name, "pb")) pb = rp; + if (!strcmp(rp->name, "pc")) map = (wl_plan_op_t *)&rp->ops[1]; + } + CHECK(pb && map && map->op == WL_PLAN_OP_MAP); + const uint32_t *original = map->project_indices; + uint32_t invalid = 99; map->project_indices = &invalid; + eval_stack_t stack; eval_stack_init(&stack); uint64_t before = reserved(&f); + /* B is newest and valid; older C must still be checked before B reads. */ + CHECK(wl_columnar_ops_cold_projection(pb, &stack, + COL_SESSION(f.session)) == ENOTSUP); + CHECK(!stack.top && reserved(&f) == before); + map->project_indices = original; + CHECK(wl_columnar_ops_cold_projection(pb, &stack, + COL_SESSION(f.session)) == 0); + eval_entry_t e = eval_stack_pop(&stack); CHECK(e.rel->nrows == 48); + CHECK(eval_stack_dispose_entry(&stack, &e) == 0 && reserved(&f) == before); + multiple_result(&f); destroy(&f); +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -1271,6 +1614,10 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + multiple_runtime_guard(); multiple_memory_floor(); + multiple_idb_filter_refusal(); + multiple_schema_audit(); multiple_right_joins(); + multiple_bindings(); multiple_seal_failures(); standalone_reads(); projection_shape(); filtered_predicates(); filtered_shape(); for (unsigned filtered = 0; filtered < 2; filtered++) { diff --git a/tests/test_wirelog_advanced_spill.c b/tests/test_wirelog_advanced_spill.c index 57c250249..cfd78125f 100644 --- a/tests/test_wirelog_advanced_spill.c +++ b/tests/test_wirelog_advanced_spill.c @@ -527,7 +527,7 @@ static void unsupported_filters(void) "keys(k) :- b(k,v), v > k.", "keys(k) :- b(k,v), v > 100, v < 200.", /* An unrelated resident filter must not inherit the cold exception. */ - "keys(k) :- a(k,v), v > 11.", + "keys(k) :- a(k,v), v + 1 > 11.", /* A predicate on the right JOIN input remains unsupported. */ "keys(k) :- a(k,v), b(k,w), w > 164." }; @@ -598,6 +598,90 @@ static void standalone_reads(void) } } +typedef struct { uint64_t keys[3]; } sealed_set_events_t; +static void sealed_set_observe(const char *name, const int64_t *row, + uint32_t width, + void *context) +{ + sealed_set_events_t *e = context; + unsigned which = !strcmp(name, "pa") ? 0u : !strcmp(name, "pb") ? 1u : 2u; + CHECK(which != 2 || !strcmp(name, "pc")); + CHECK(width == 1 && row[0] >= 0 && row[0] < 64); + uint64_t bit = UINT64_C(1) << (unsigned)row[0]; + CHECK(!(e->keys[which] & bit)); e->keys[which] |= bit; +} +static void sealed_sets(void) +{ + const char *text = + ".decl a(k:int64,v:int64)\n.decl b(k:int64,x:int64,v:int64)\n" + ".decl c(k:int64,v:int64)\n" + ".decl pa(k:int64)\n.decl pb(k:int64)\n.decl pc(k:int64)\n" + "pa(k) :- a(k,v), v >= 132.\npb(k) :- b(k,x,v), v >= 216.\n" + "pc(k) :- c(k,v).\n"; + const char *orders[][3] = {{"a", "b", "c"}, {"b", "a", "c"}, + {"c", "b", "a"}}; + for (unsigned selectivity = 0; selectivity < 3; selectivity++) + for (unsigned empty = 0; empty < 2; empty++) + for (unsigned order = 0; order < 3; order++) + for (unsigned count = 0; count < 4; count++) { + wirelog_program_t *p = parse(text); + wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, + WIRELOG_BACKEND_DEFAULT, 1, &o, &s) == WIRELOG_OK); + for (unsigned i = 0; i < (empty ? 1u : 64u); i++) { + int64_t a[] = {(int64_t)i, + (selectivity == 0 ? 100 : selectivity == + 1 ? 0 : 1000)+(int64_t)i}; + int64_t b[] = {(int64_t)i, -(int64_t)i, + (selectivity == 0 ? 200 : selectivity == + 1 ? 0 : 1000)+(int64_t)i}; + int64_t c[] = {(int64_t)i, 300+(int64_t)i}; + CHECK(wirelog_session_insert(s, "a", a, 1, + 2) == WIRELOG_OK); + CHECK(wirelog_session_insert(s, "b", b, 1, + 3) == WIRELOG_OK); + CHECK(wirelog_session_insert(s, "c", c, 1, + 2) == WIRELOG_OK); + if (empty) { + CHECK(wirelog_session_remove(s, "a", a, 1, + 2) == WIRELOG_OK); + CHECK(wirelog_session_remove(s, "b", b, 1, + 3) == WIRELOG_OK); + CHECK(wirelog_session_remove(s, "c", c, 1, + 2) == WIRELOG_OK); + } + } + for (unsigned i = 0; i < count; i++) { + CHECK((i % 2 ? wirelog_session_seal_join_input(s, + orders[order][i]) : + wirelog_session_seal_input(s, + orders[order][i])) == WIRELOG_OK); + check_mutation(s); + } + if (count) CHECK(wirelog_session_seal_input(s, + orders[order][0]) == WIRELOG_ERR_EXEC); + if (order == + 1) CHECK(wirelog_session_step(s) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + sealed_set_events_t e = {0}; + CHECK(wirelog_session_snapshot(s, sealed_set_observe, + &e) == WIRELOG_OK); + CHECK(e.keys[0] == (empty || + selectivity == 1 ? 0 : selectivity == + 2 ? UINT64_MAX : UINT64_MAX << 32)); + CHECK(e.keys[1] == (empty || + selectivity == 1 ? 0 : selectivity == + 2 ? UINT64_MAX : UINT64_MAX << 16)); + CHECK(e.keys[2] == (empty ? 0 : UINT64_MAX)); + } + CHECK(wirelog_session_seal_input(s, + "c") == WIRELOG_ERR_EXEC); + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + int main(void) { /* Issue #2287: a session created with a nonzero WIRELOG_JOIN_BATCH_BYTES @@ -617,7 +701,7 @@ int main(void) CHECK(mkdtemp(directory)); #endif descriptors(); lifecycle(); refusals(); typed_projection(); inline_facts(); - standalone_reads(); output_floor(); mixed_projection(); + sealed_sets(); standalone_reads(); output_floor(); mixed_projection(); unsupported_filters(); join_batch_knob(); #ifdef _WIN32 diff --git a/wirelog/columnar/eval_plan.c b/wirelog/columnar/eval_plan.c index 49d40db79..8f1367106 100644 --- a/wirelog/columnar/eval_plan.c +++ b/wirelog/columnar/eval_plan.c @@ -465,14 +465,15 @@ col_eval_relation_plan(const wl_plan_relation_t *rplan, eval_stack_t *stack, wl_col_session_t *sess) { if (sess->cold_relation && rplan->ops) { - const char *name = sess->cold_relation->name; for (uint32_t i = 0; i < rplan->op_count; i++) { - if (!rplan->ops[i].relation_name || - strcmp(rplan->ops[i].relation_name, name)) continue; + wl_columnar_session_cold_relation_t *cold = + wl_columnar_session_cold_find( + sess, rplan->ops[i].relation_name); + if (!cold) continue; if (wl_columnar_session_cold_execution_check(sess) || - !wl_columnar_eval_plan_cold_projection(rplan, name, - sess->cold_relation->ncols, wl_columnar_batch_payload_schema( - sess->cold_relation->payload, NULL, NULL))) return ENOTSUP; + !wl_columnar_eval_plan_cold_projection(rplan, cold->name, + cold->ncols, wl_columnar_batch_payload_schema( + cold->payload, NULL, NULL))) return ENOTSUP; int rc = wl_columnar_ops_cold_projection(rplan, stack, sess); return rc ? rc : col_op_consolidate(stack, sess); } diff --git a/wirelog/columnar/internal.h b/wirelog/columnar/internal.h index 594b9f8a4..d92410be9 100644 --- a/wirelog/columnar/internal.h +++ b/wirelog/columnar/internal.h @@ -2341,6 +2341,9 @@ struct wl_columnar_retained_eval_entry; * All metadata shares reservation; payload owns types and rows. Borrowed fields * and readers may not escape a serialized session operation. */ typedef struct wl_columnar_session_cold_relation { + struct wl_columnar_session_cold_relation *next; + /* NULL in production; keep layout identical across test-hook builds. */ + col_rel_t *retained_source; wl_columnar_memory_reservation_t reservation; struct wl_columnar_batch_payload *payload; col_relation_snapshot_t source_snapshot; @@ -2695,7 +2698,7 @@ typedef struct wl_col_session_t { uint64_t spill_join_batch_bytes; uint64_t spill_join_index_bytes; uint64_t spill_join_selected_count; - wl_columnar_session_cold_relation_t *cold_relation; + wl_columnar_session_cold_relation_t *cold_relation; /* Owned binding list. */ bool cold_seal_closed; /* First evaluation attempt permanently closes seal. */ /* Issue #1380: memory instrumentation state that is not part of the * ledger proper. @@ -4934,8 +4937,8 @@ int session_remove_rel(wl_col_session_t *sess, const char *name); /* Explicit internal pre-evaluation opt-in, under external session lifecycle * serialization. Caller keeps the plan immutable through session destruction. - * One canonical numeric EDB used only by supported right JOINs or complete - * direct/filtered projections may be sealed. Success removes its resident registry owner and makes ALL fact and + * Canonical numeric EDBs used only by supported right JOINs or complete + * direct/filtered projections may be sealed individually. Success removes its resident registry owner and makes ALL fact and * compound mutation unsupported, including zero-row requests. Failed sealing * preserves the source/registry and refunds private work. * No readers may escape synchronous operations; callbacks must not reenter @@ -4948,6 +4951,8 @@ wl_columnar_spill_status_t wl_columnar_session_seal_input( wl_columnar_spill_status_t wl_columnar_session_seal_join_input( wl_session_t *session, const char *name, wl_columnar_spill_cancel_fn cancel, void *context); +wl_columnar_session_cold_relation_t *wl_columnar_session_cold_find( + const wl_col_session_t *session, const char *name); int wl_columnar_session_cold_execution_check(const wl_col_session_t *session); /* Valid only after the complete cold projection body has been audited. */ typedef struct { diff --git a/wirelog/columnar/join.c b/wirelog/columnar/join.c index d9a87c17d..fdae12a10 100644 --- a/wirelog/columnar/join.c +++ b/wirelog/columnar/join.c @@ -1500,7 +1500,8 @@ wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, uint64_t started = now_ns(); sess->profile.join_calls++; #endif - const wl_columnar_session_cold_relation_t *cold = sess->cold_relation; + const wl_columnar_session_cold_relation_t *cold = + wl_columnar_session_cold_find(sess, op->right_relation); int rc = wl_columnar_session_cold_execution_check(sess); if (rc || op->key_count != 1 || op->materialized || op->right_filter_expr.size || left->is_delta || @@ -1619,8 +1620,7 @@ wl_columnar_join_op(const wl_plan_op_t *op, eval_stack_t *stack, int pop_rc = eval_stack_pop_relation(stack, &left_e); if (pop_rc != 0) return pop_rc; - if (sess->cold_relation && op->right_relation && - strcmp(sess->cold_relation->name, op->right_relation) == 0) + if (wl_columnar_session_cold_find(sess, op->right_relation)) return wl_columnar_join_cold_run(sess, op, stack, &left_e); /* right_filtered: non-NULL only when right was pool-allocated by diff --git a/wirelog/columnar/ops.c b/wirelog/columnar/ops.c index 6f5468785..f295645fe 100644 --- a/wirelog/columnar/ops.c +++ b/wirelog/columnar/ops.c @@ -118,8 +118,7 @@ col_op_variable(const wl_plan_op_t *op, eval_stack_t *stack, } if (!op->relation_name) return ENOENT; - if (sess->cold_relation && - !strcmp(sess->cold_relation->name, op->relation_name)) return ENOTSUP; + if (wl_columnar_session_cold_find(sess, op->relation_name)) return ENOTSUP; col_rel_t *full_rel = session_find_rel(sess, op->relation_name); if (!full_rel) return ENOENT; @@ -359,15 +358,17 @@ wl_columnar_ops_cold_projection(const wl_plan_relation_t *plan, eval_stack_t *stack, wl_col_session_t *sess) { wl_columnar_eval_plan_cold_projection_t decoded; - if (!sess || !stack || !sess->cold_relation || + wl_columnar_session_cold_relation_t *cold = plan && plan->ops && + plan->op_count + ? wl_columnar_session_cold_find(sess, + plan->ops[0].relation_name) : NULL; + if (!sess || !stack || !cold || wl_columnar_session_cold_execution_check(sess) || !wl_columnar_eval_plan_cold_projection_decode(plan, - sess->cold_relation->name, - sess->cold_relation->ncols, wl_columnar_batch_payload_schema( - sess->cold_relation->payload, NULL, NULL), + cold->name, cold->ncols, wl_columnar_batch_payload_schema( + cold->payload, NULL, NULL), &decoded)) return ENOTSUP; const wl_plan_op_t *op = decoded.map; - wl_columnar_session_cold_relation_t *cold = sess->cold_relation; uint32_t width = op->project_count; uint64_t bytes; if (!wl_columnar_memory_size_mul(width, diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 008dc9e44..8a8f83f25 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -596,7 +596,6 @@ session_find_rel(wl_col_session_t *sess, const char *name) * Policy reset cannot release or overwrite the independent retained owner. */ bool wl_columnar_session_test_retain_cold_source; static wl_col_session_t *wl_columnar_session_test_cold_owner; -static col_rel_t *wl_columnar_session_test_cold_source; bool wl_columnar_session_test_has_retained_cold_source(const wl_col_session_t *s) { @@ -606,6 +605,61 @@ wl_columnar_session_test_has_retained_cold_source(const wl_col_session_t *s) } #endif +wl_columnar_session_cold_relation_t * +wl_columnar_session_cold_find(const wl_col_session_t *s, const char *name) +{ + if (!s || !name) return NULL; + for (wl_columnar_session_cold_relation_t *c = s->cold_relation; c; + c = c->next) + if (!strcmp(c->name, name)) return c; + return NULL; +} + +/* Eligibility of another rule is independent of the sealing order. Resolve + * its own complete scalar schema, never the candidate's schema. Discovery is + * read-only: a failed preflight must not allocate a resident registry hash. */ +static bool +wl_columnar_session_cold_projection_rule(const wl_col_session_t *s, + const wl_plan_relation_t *rp) +{ + if (!rp || !rp->ops || !rp->op_count || + rp->ops[0].op != WL_PLAN_OP_VARIABLE || !rp->ops[0].relation_name) + return false; + const char *name = rp->ops[0].relation_name; + bool edb = false; + for (uint32_t i = 0; i < s->plan->edb_count; i++) + if (s->plan->edb_relations && s->plan->edb_relations[i] && + !strcmp(s->plan->edb_relations[i], name)) edb = true; + if (!edb) return false; + wl_columnar_session_cold_relation_t *c = wl_columnar_session_cold_find(s, + name); + uint32_t width = 0; + const wirelog_column_type_t *types = NULL; + if (c) { + width = c->ncols; + types = wl_columnar_batch_payload_schema(c->payload, NULL, NULL); + } else { + const col_rel_t *r = NULL; + for (uint32_t i = 0; i < s->nrels; i++) + if (s->rels[i] && !strcmp(s->rels[i]->name, name)) { + r = s->rels[i]; break; + } + if (!r || !r->ncols || r->timestamps || r->has_graph_column || + r->compound_kind != WIRELOG_COMPOUND_KIND_NONE || + r->compound_count || r->compound_arity_map || + r->compound_arity_len || r->inline_physical_offset) return false; + width = r->ncols; types = r->column_types; + for (uint32_t i = 0; types && i < width; i++) + if (types[i] != WIRELOG_TYPE_INT32 && + types[i] != WIRELOG_TYPE_INT64 && + types[i] != WIRELOG_TYPE_UINT32 && + types[i] != WIRELOG_TYPE_UINT64 && + types[i] != WIRELOG_TYPE_FLOAT && types[i] != WIRELOG_TYPE_BOOL) + return false; + } + return wl_columnar_eval_plan_cold_projection(rp, name, width, types); +} + /* Cold bindings never enter the raw relation registry. Only an audited, * immutable plan may access them through bounded JOIN/projection readers. */ static bool @@ -627,9 +681,11 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, const wl_plan_relation_t *rp = &sp->relations[ri]; if (!rp->name || strcmp(rp->name, name) == 0 || !rp->ops) return false; - bool projection = wl_columnar_eval_plan_cold_projection(rp, - name, ncols, types); - if (projection) consumer = true; + bool projection = wl_columnar_session_cold_projection_rule(sess, + rp); + bool selected_projection = projection && + !strcmp(rp->ops[0].relation_name, name); + if (selected_projection) consumer = true; for (uint32_t oi = 0; oi < rp->op_count; oi++) { const wl_plan_op_t *op = &rp->ops[oi]; /* Reject all opaque rewrites, even if top-level names hide @@ -642,7 +698,7 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, (op->delta_mode != WL_DELTA_AUTO && op->delta_mode != WL_DELTA_FORCE_FULL)) return false; if (op->relation_name && strcmp(op->relation_name, name) == 0 && - !projection) + !selected_projection) return false; if (!op->right_relation || strcmp(op->right_relation, name) != 0) @@ -680,12 +736,15 @@ int wl_columnar_session_cold_execution_check(const wl_col_session_t *s) { if (!s->cold_relation) return 0; - const wl_columnar_session_cold_relation_t *c = s->cold_relation; - const wirelog_column_type_t *types = wl_columnar_batch_payload_schema( - c->payload, NULL, NULL); - return wl_columnar_session_cold_mode(s) && - wl_columnar_session_cold_plan(s, c->name, c->ncols, - (const char *const *)c->col_names, types) ? 0 : ENOTSUP; + if (!wl_columnar_session_cold_mode(s)) return ENOTSUP; + for (const wl_columnar_session_cold_relation_t *c = s->cold_relation; c; + c = c->next) { + const wirelog_column_type_t *types = wl_columnar_batch_payload_schema( + c->payload, NULL, NULL); + if (!wl_columnar_session_cold_plan(s, c->name, c->ncols, + (const char *const *)c->col_names, types)) return ENOTSUP; + } + return 0; } static void @@ -710,10 +769,12 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, wl_col_session_t *s = COL_SESSION(session); #ifdef WL_SESSION_TEST_HOOKS bool retain = wl_columnar_session_test_retain_cold_source; - if (retain && wl_columnar_session_test_cold_owner) + if (retain && wl_columnar_session_test_cold_owner && + wl_columnar_session_test_cold_owner != s) return WL_COLUMNAR_SPILL_BUSY; #endif - if (s->base.input_load_failed || s->cold_relation || s->cold_seal_closed || + if (s->base.input_load_failed || wl_columnar_session_cold_find(s, name) || + s->cold_seal_closed || s->has_evaluated || s->plain_step_completion_pending || s->uncommitted_strata_done || s->cleanup_active || s->cleanup_pending || s->source_leases || @@ -724,7 +785,9 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, s->sarr_count || s->filt_arr_count || s->filt_cache_count || s->mat_cache.count || s->last_removed_relation) return WL_COLUMNAR_SPILL_BUSY; - if (!wl_columnar_session_cold_mode(s)) return WL_COLUMNAR_SPILL_UNSUPPORTED; + if (!wl_columnar_session_cold_mode(s) || + wl_columnar_session_cold_execution_check(s)) + return WL_COLUMNAR_SPILL_UNSUPPORTED; /* Linear lookup avoids allocating a hash merely to refuse this operation. */ uint32_t slot = 0; while (slot < s->nrels && (!s->rels[slot] || @@ -816,12 +879,13 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, #ifdef WL_SESSION_TEST_HOOKS if (retain) { wl_columnar_session_test_cold_owner = s; - wl_columnar_session_test_cold_source = r; + c->retained_source = r; } #endif s->rels[slot] = NULL; session_rel_free_hash(s); if (inserted_name) s->last_inserted_relation = c->name; + c->next = s->cold_relation; s->cold_relation = c; s->base.input_sealed = true; return WL_COLUMNAR_SPILL_OK; @@ -4676,9 +4740,13 @@ col_session_destroy(wl_session_t *session) #endif #ifdef WL_SESSION_TEST_HOOKS if (wl_columnar_session_test_cold_owner == sess) { - if (col_rel_destroy_checked( - wl_columnar_session_test_cold_source)) abort(); - wl_columnar_session_test_cold_source = NULL; + for (wl_columnar_session_cold_relation_t *c = sess->cold_relation; c; + c = c->next) { + if (c->retained_source && + col_rel_destroy_checked(c->retained_source)) + abort(); + c->retained_source = NULL; + } wl_columnar_session_test_cold_owner = NULL; } #endif @@ -4774,8 +4842,11 @@ col_session_destroy(wl_session_t *session) : 0ULL); wl_arena_compound_arena_retire(sess->compound_arena); sess->compound_arena = NULL; - wl_columnar_session_cold_free(sess->cold_relation); - sess->cold_relation = NULL; + while (sess->cold_relation) { + wl_columnar_session_cold_relation_t *c = sess->cold_relation; + sess->cold_relation = c->next; + wl_columnar_session_cold_free(c); + } if (sess->owns_spill_store) wl_columnar_spill_store_retire(sess->spill_store); wl_columnar_memory_governor_ref_release(sess->memory_governor); diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index 647c3f503..24de9aa0c 100644 --- a/wirelog/wirelog-advanced.h +++ b/wirelog/wirelog-advanced.h @@ -229,10 +229,14 @@ wirelog_session_create_with_options(wirelog_program_t *program, * opaque/materialized rewrites, worker/control/delta modes or delta callback. * Optimized plans may therefore be ineligible. The session must also not have * been created with WIRELOG_JOIN_BATCH_BYTES on (see - * wirelog_session_options_v1_t). Only one input may be sealed. + * wirelog_session_options_v1_t). Multiple loaded inputs may be + * sealed individually, in any order, before the first evaluation attempt. * * Success reclaims the resident source and makes all fact/compound mutation - * unsupported for the entire session. The binding is reused by supported + * unsupported for the entire session. Further sealing of other already-loaded + * eligible inputs is allowed until evaluation. Each seal is transactional: + * failure preserves earlier bindings and the failed input's resident source. + * Re-sealing an already sealed name is an error. The binding is reused by supported * STEP/SNAPSHOT calls until destruction; sealing cannot be undone. A refusal * preserves the original source and allows retry only while the session is * still eligible, and a refusal because WIRELOG_JOIN_BATCH_BYTES was on lasts From 7afbd2e4b72131ac348470d5d45ce8672c527234 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 16:37:03 +0900 Subject: [PATCH 3/9] feat(columnar): join sealed inputs in either position --- CHANGELOG.md | 5 + docs/MEMORY.md | 31 +- docs/SEMANTICS.md | 8 +- tests/test_session_cold_relation.c | 548 ++++++++++++++++++++++++++-- tests/test_wirelog_advanced_spill.c | 82 +++++ wirelog/columnar/eval_plan.c | 17 +- wirelog/columnar/internal.h | 6 + wirelog/columnar/join.c | 253 ++++++++++--- wirelog/columnar/session.c | 9 +- wirelog/wirelog-advanced.h | 9 +- 10 files changed, 869 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27be9e17..7259b1402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to wirelog are documented in this file. ### Added +- **Sealed JOIN inputs in either position** (#1372): bounded readers now consume + cold left, both-cold and self-JOIN inputs. Audited chains continue through + admitted resident intermediates, preserving single-INT64-key semantics, + numeric lane types, transactional failure cleanup and explicit output floors. + - **Multiple sealed inputs** (#1372): independently seal already-loaded numeric sources in either order, retaining earlier bindings on a later failure. Per-source lookup, destruction and narrow projection schema auditing preserve diff --git a/docs/MEMORY.md b/docs/MEMORY.md index b4d161826..a3d372b6b 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -4751,11 +4751,18 @@ and another source's valid projection cannot authorize an unused candidate. The supported plan is serial, nonrecursive, and contains only audited VARIABLE/JOIN/MAP/CONSOLIDATE operations, plus the restricted projection FILTER -described below. The cold name must have an eligible consumer: a JOIN right -input or the exact direct/filtered projection body below; it cannot be a relation head +described below. The cold name must have an eligible consumer: a JOIN input +or the exact direct/filtered projection body below; it cannot be a relation head or an arbitrary VARIABLE consumer. Eligible JOINs use one INT64 key, scalar numeric rows, no timestamps and no right filter. Multiple eligible JOINs -may consume the same binding. Spill batch/index caps must be positive. Opaque +may consume the same binding. A cold left source requires an initial +VARIABLE/JOIN pair. Both inputs can be sealed, and a self-JOIN acquires two +independent reader pins over one persistent owner. A subsequent JOIN consumes +an admitted resident intermediate. FILTER/MAP before the first cold JOIN, +multi-key/cross/theta JOINs and non-INT64 keys are refused. Available source +schemas and projection bounds are checked before retirement and again at +execution; an unloaded resident counterpart is checked when its schema exists. +Spill batch/index caps must be positive. Opaque rewrites, workers, controlled execution, delta callbacks and competing JOIN batch modes are refused. The plan must remain immutable for the session lifetime, and session operations remain externally serialized. The advanced public opt-in @@ -4767,11 +4774,25 @@ an otherwise usable sealed session; genuine partial-load errors still poison. Supported STEP/SNAPSHOT execution resolves the binding before the missing-right case and routes it through the capped payload producer without rebuilding a resident arrangement. Schema/projection ownership is admitted before private -left transfer. Readers hold separate cursors and release pins before binding +resident source transfer. Resident counterparts are privately captured; sealed +inputs are borrowed without rehydration. Failed attempts drop reader pins before +releasing only temporary payload owners. Missing resident right inputs preserve +the ordinary empty-result typing contract. Readers hold separate cursors and release pins before binding cleanup. Repeated queries reuse the same owned spill segments; session teardown releases those segments and their governed metadata. Final output remains resident, so successful input retirement does not promise arbitrary result size. +`test_session_cold_relation` also compares all resident/cold input directions, +both seal orders, duplicate/skew and empty inputs, self-JOIN, repeated consumers, +and a two-JOIN chain with a direct scan. Fixed-allowance controls separately +prove left-only and two-owner reclamation against deliberately retained physical +allocations. A direct both-cold JOIN compares constructor/reader/index scratch +at two source cardinalities with the same caps and no output rows. A real +admitted pressure allocation proves constructor refusal and cleanup; a separate +many-match case proves the resident output floor. A no-match final result with +a large first JOIN separately proves the resident intermediate floor. These are bounded-input +claims, not a guarantee that arbitrary output or intermediate sizes fit. + `test_session_cold_relation` supplies the lifecycle and causal budget evidence. A separate calibration run chooses an allowance for two fresh sessions with the same parsed plan, facts and caps: 128 left rows, 32,768 right rows, and 8,192 exact @@ -4919,7 +4940,7 @@ integer overflow. An optional single FILTER between VARIABLE and MAP accepts one INT64 column compared with one integer literal using `=`, `!=`, `<`, `>`, `<=` or `>=` (column on the left). For example, `keys(k) :- b(k,v), v >= 164.` filters on a column that need not appear in the output. Other predicates, -arithmetic, multiple filters, concatenation, cold left JOINs, +arithmetic, multiple filters, concatenation, FILTER/MAP before a cold JOIN, opaque rewrites and other raw-column consumers remain explicitly unsupported. A raw VARIABLE request for a known cold name returns ENOTSUP rather than treating the removed registry entry as empty. No synthetic relation with missing columns diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index 9f7fa8265..886302fdb 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -256,8 +256,12 @@ identical behavior. A sealed JOIN input may additionally feed an exact `VARIABLE → MAP → CONSOLIDATE` direct column-projection rule. Column indices may repeat or reorder. One FILTER between VARIABLE and MAP may compare one INT64 column with one integer literal (`=`, `!=`, `<`, `>`, `<=`, `>=`), with the -column on the left. Other predicates, arbitrary expressions and cold left JOINs remain -unsupported. An eligible direct/filtered projection alone is sufficient; a +column on the left. Other predicates and arbitrary cold expressions remain +unsupported. An initial `VARIABLE → JOIN` may instead consume a cold left +source, including two sealed sources or the same binding on both sides. +Each JOIN uses one INT64 equality key. Chained JOINs continue from an admitted +resident intermediate; FILTER/MAP before the first cold JOIN, multi-key/cross/ +theta JOINs, non-INT64 keys and right filters remain unsupported. An eligible direct/filtered projection alone is sufficient; a JOIN consumer is not required. Reads use independent bounded pins; projected output and consolidation remain resident. Output capacity is admitted for the full source row count even if no row passes. diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 1aa48c833..4635564ec 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -1,4 +1,4 @@ -/* Persistent #1372 right-only EDB reclamation through the ordinary evaluator. */ +/* Persistent #1372 sealed-input reclamation through the ordinary evaluator. */ #include "columnar/internal.h" #include "columnar/batch_payload.h" #include "columnar/columnar_nanoarrow.h" @@ -374,43 +374,56 @@ static void empty_and_multiple(void) static void typed_observe(const char *name, const int64_t *row, uint32_t cols, void *ctx) { - CHECK(!strcmp(name, "result") && cols == 4); + CHECK(!strcmp(name, "result") && cols == 5); CHECK((uint64_t)row[0] == UINT64_C(0x3ff8000000000000) && (uint64_t)row[1] == UINT64_C(0xfffffffffffffff0) && - row[2] == 7 && row[3] == row[0]); + row[2] == 7 && row[3] == row[0] && row[4] == 1); (*(unsigned *)ctx)++; } static void typed_projection(void) { - fixture_t f = fixture( - ".decl a(k: int64)\n.decl b(k: int64, v: float, u: int64)\n" - ".decl result(v: float, u: int64, k: int64, again: float)\n" - "result(v,u,k,v) :- a(k),b(k,v,u).\n", 4*1024*1024, 1024*1024); - int64_t left[] = {7}; - uint64_t bits[] = {7, UINT64_C(0x3ff8000000000000), - UINT64_C(0xfffffffffffffff0)}; - int64_t right[3]; memcpy(right, bits, sizeof(right)); - CHECK(wl_session_insert(f.session, "a", left, 1, 1) == 0); - CHECK(wl_session_insert(f.session, "b", right, 1, 3) == 0); - /* The parser has no uint64 declaration spelling; the internal host may - * provide an explicit physical numeric schema. */ - wirelog_column_type_t types[] = {WIRELOG_TYPE_INT64, WIRELOG_TYPE_FLOAT, - WIRELOG_TYPE_UINT64}; - CHECK(col_rel_set_column_types(session_find_rel(COL_SESSION(f.session), - "b"), - types, 3) == 0); - CHECK(wl_columnar_session_seal_join_input(f.session, "b", NULL, - NULL) == OK); - unsigned count = 0; - CHECK(wl_session_snapshot(f.session, typed_observe, - &count) == 0 && count == 1); - col_rel_t *out = session_find_rel(COL_SESSION(f.session), "result"); - CHECK(out && out->column_types && - out->column_types[0] == WIRELOG_TYPE_FLOAT && - out->column_types[1] == WIRELOG_TYPE_UINT64 && - out->column_types[2] == WIRELOG_TYPE_INT64 && - out->column_types[3] == WIRELOG_TYPE_FLOAT); - destroy(&f); + for (unsigned direction = 0; direction < 5; direction++) { + fixture_t f = fixture( + ".decl a(k: int64, f: int64)\n.decl b(k: int64, v: float, u: int64)\n" + ".decl result(v: float, u: int64, k: int64, again: float, flag: int64)\n" + "result(v,u,k,v,f) :- a(k,f),b(k,v,u).\n", 4*1024*1024, 1024*1024); + int64_t left[] = {7, 1}; + uint64_t bits[] = {7, UINT64_C(0x3ff8000000000000), + UINT64_C(0xfffffffffffffff0)}; + int64_t right[3]; memcpy(right, bits, sizeof(right)); + CHECK(wl_session_insert(f.session, "a", left, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", right, 1, 3) == 0); + /* The parser has no uint64 declaration spelling; the internal host may + * provide an explicit physical numeric schema. */ + wirelog_column_type_t types[] = {WIRELOG_TYPE_INT64, WIRELOG_TYPE_FLOAT, + WIRELOG_TYPE_UINT64}; + CHECK(col_rel_set_column_types(session_find_rel(COL_SESSION(f.session), + "b"), + types, 3) == 0); + wirelog_column_type_t left_types[] = {WIRELOG_TYPE_INT64, + WIRELOG_TYPE_BOOL}; + CHECK(col_rel_set_column_types(session_find_rel(COL_SESSION(f.session), + "a"), left_types, 2) == 0); + if (direction == 4) CHECK(wl_columnar_session_seal_input(f.session, "b", + NULL, NULL) == OK); + if (direction == 1 || + direction >= 3) CHECK(wl_columnar_session_seal_input(f.session, "a", + NULL, NULL) == OK); + if (direction == 2 || + direction == 3) CHECK(wl_columnar_session_seal_input(f.session, "b", + NULL, NULL) == OK); + unsigned count = 0; + CHECK(wl_session_snapshot(f.session, typed_observe, + &count) == 0 && count == 1); + col_rel_t *out = session_find_rel(COL_SESSION(f.session), "result"); + CHECK(out && out->column_types && + out->column_types[0] == WIRELOG_TYPE_FLOAT && + out->column_types[1] == WIRELOG_TYPE_UINT64 && + out->column_types[2] == WIRELOG_TYPE_INT64 && + out->column_types[3] == WIRELOG_TYPE_FLOAT && + out->column_types[4] == WIRELOG_TYPE_BOOL); + destroy(&f); + } } static void quota_and_alias(void) { @@ -1605,6 +1618,472 @@ static void multiple_runtime_guard(void) multiple_result(&f); destroy(&f); } +/* Direction matrix fixes the physical first input so optimizer reordering + * cannot turn cold-left coverage into another cold-right test. */ +static wl_plan_relation_t *join_body(fixture_t *f, const char *name) +{ + for (uint32_t i = 0; i < f->plan->stratum_count; i++) + for (uint32_t j = 0; j < f->plan->strata[i].relation_count; j++) { + wl_plan_relation_t *rp = + (wl_plan_relation_t *)&f->plan->strata[i].relations[j]; + if (!strcmp(rp->name, name)) return rp; + } + CHECK(false); return NULL; +} +static void seal_direction(fixture_t *f, unsigned direction) +{ + if (direction == 4) CHECK(wl_columnar_session_seal_input(f->session, "b", + NULL, NULL) == OK); + if (direction == 1 || + direction >= 3) CHECK(wl_columnar_session_seal_input(f->session, "a", + NULL, NULL) == OK); + if (direction == 2 || + direction == 3) CHECK(wl_columnar_session_seal_input(f->session, "b", + NULL, NULL) == OK); +} +typedef struct { unsigned count; bool seen[144]; } direction_events_t; +static void direction_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + direction_events_t *e = context; + CHECK(!strcmp(name, "result") && width == 3); + CHECK(row[1] >= 10 && row[1] < 22 && row[2] >= 100 && row[2] < 112); + unsigned x = (unsigned)(row[1] - 10), y = (unsigned)(row[2] - 100); + CHECK(x / 6 == y / 6 && row[0] == (x < 6 ? INT64_MIN : INT64_MAX)); + unsigned ordinal = x*12 + y; + CHECK(!e->seen[ordinal]); e->seen[ordinal] = true; e->count++; +} +static void direction_matrix(void) +{ + for (unsigned shape = 0; shape < 5; shape++) + for (unsigned direction = 0; direction < 5; direction++) { + fixture_t f = fixture(query, 4*1024*1024, 1024*1024); + wl_plan_relation_t *rp = join_body(&f, "result"); + CHECK(rp->ops[0].op == WL_PLAN_OP_VARIABLE && + !strcmp(rp->ops[0].relation_name, "a") && + rp->ops[1].op == WL_PLAN_OP_JOIN && + !strcmp(rp->ops[1].right_relation, "b")); + for (int64_t i = 0; i < 12; i++) { + int64_t a[] = {i < 6 ? INT64_MIN : INT64_MAX, 10+i}; + int64_t b[] = {shape == 1 ? 0 : a[0], 100+i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + if (shape == 2 || shape == 4) CHECK(wl_session_remove(f.session, + "a", a, 1, 2) == 0); + if (shape == 3 || shape == 4) CHECK(wl_session_remove(f.session, + "b", b, 1, 2) == 0); + } + seal_direction(&f, direction); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + if (direction == 4) CHECK(wl_session_step(f.session) == 0); + for (unsigned repeat = 0; repeat < 2; repeat++) { + direction_events_t e = {0}; + CHECK(wl_session_snapshot(f.session, direction_observe, + &e) == 0); + CHECK(e.count == (shape ? 0 : 72)); + if (direction) CHECK(s->spill_join_selected_count == 1); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + } + destroy(&f); + } +} +static void self_observe(const char *name, const int64_t *row, uint32_t width, + void *context) +{ + direction_events_t *e = context; + CHECK(!strcmp(name, "result") && width == 3 && row[0] == 7); + CHECK(row[1] >= 0 && row[1] < 12 && row[2] >= 0 && row[2] < 12); + unsigned ordinal = (unsigned)(row[1]*12 + row[2]); + CHECK(!e->seen[ordinal]); e->seen[ordinal] = true; e->count++; +} +static void self_join(void) +{ + const char *text = + ".decl a(k:int64,v:int64)\n.decl result(k:int64,x:int64,y:int64)\n" + "result(k,x,y+0) :- a(k,x),a(k,y).\n"; + for (unsigned cold = 0; cold < 2; cold++) { + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + wl_plan_relation_t *rp = join_body(&f, "result"); + CHECK(rp->ops[0].op == WL_PLAN_OP_VARIABLE && + !strcmp(rp->ops[0].relation_name, "a") && + rp->ops[1].op == WL_PLAN_OP_JOIN && + !strcmp(rp->ops[1].right_relation, "a")); + for (int64_t i = 0; i < 12; i++) { + int64_t row[] = {7, i}; + CHECK(wl_session_insert(f.session, "a", row, 1, 2) == 0); + } + if (cold) CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == OK); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + if (cold) { + CHECK(s->cold_relation && !s->cold_relation->next); + read_fault = 7; unsigned count = 0; + CHECK(wl_session_snapshot(f.session, count_only, + &count) == EIO && count == 0); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + read_fault = 0; + } + for (unsigned repeat = 0; repeat < 2; repeat++) { + direction_events_t e = {0}; + CHECK(wl_session_snapshot(f.session, self_observe, + &e) == 0 && e.count == 144); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + } + destroy(&f); + } +} +static const char *chain_query = + ".decl a(k:int64,v:int64)\n.decl b(k:int64,v:int64)\n.decl c(k:int64,v:int64)\n" + ".decl result(k:int64,x:int64,y:int64,z:int64)\n" + "result(k,x,y,z+0) :- a(k,x),b(k,y),c(y,z).\n" + ".decl scan(k:int64)\nscan(k) :- a(k,x).\n" + ".decl second(k:int64,x:int64,y:int64)\nsecond(k,x,y+0) :- a(k,x),b(k,y).\n"; +typedef struct { unsigned bits[3]; } chain_events_t; +static void chain_observe(const char *name, const int64_t *row, uint32_t width, + void *context) +{ + chain_events_t *e = context; + unsigned idx = !strcmp(name, "result") ? 0u : !strcmp(name, + "scan") ? 1u : 2u; + CHECK(idx != 2 || !strcmp(name, "second")); + CHECK(row[0] >= 0 && row[0] < 8); + CHECK(width == (idx == 0 ? 4u : idx == 1 ? 1u : 3u)); + if (idx != 1) CHECK(row[1] == row[0]+10 && row[2] == row[0]+100); + if (idx == 0) CHECK(row[3] == row[0]+1000); + unsigned bit = 1u << (unsigned)row[0]; + CHECK(!(e->bits[idx]&bit)); e->bits[idx] |= bit; +} +static void chained_consumers(void) +{ + for (unsigned order = 0; order < 3; order++) { + fixture_t f = fixture(chain_query, 4*1024*1024, 1024*1024); + wl_plan_relation_t *rp = join_body(&f, "result"); + CHECK(rp->ops[0].op == WL_PLAN_OP_VARIABLE && + !strcmp(rp->ops[0].relation_name, + "a") && rp->ops[1].op == WL_PLAN_OP_JOIN); + unsigned joins = 0; + for (uint32_t i = 0; i < rp->op_count; + i++) joins += rp->ops[i].op == WL_PLAN_OP_JOIN; + CHECK(joins == 2); + for (int64_t i = 0; i < 8; i++) { + int64_t a[] = {i, 10+i}, b[] = {i, 100+i}, c[] = {100+i, 1000+i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "c", c, 1, 2) == 0); + } + if (order) { + const char *names[] = {"a", "b", "c"}; + for (unsigned i = 0; i < 3; + i++) CHECK(wl_columnar_session_seal_input(f.session, + names[order == 1 ? i : 2-i], NULL, NULL) == OK); + } + for (unsigned repeat = 0; repeat < 2; repeat++) { + chain_events_t e = {0}; + CHECK(wl_session_snapshot(f.session, chain_observe, &e) == 0); + CHECK(e.bits[0] == 255 && e.bits[1] == 255 && e.bits[2] == 255); + } + if (order) CHECK(COL_SESSION(f.session)->spill_join_selected_count == + 3); + destroy(&f); + } +} +static void directional_failures(void) +{ + for (unsigned direction = 1; direction <= 4; direction++) + for (unsigned fault = 0; fault < 11; fault++) { + const unsigned failures[] = {1, 2, 3, 4, 5, 6, 7, 8, 16, 80, 512}; + unsigned nth = failures[fault]; + fixture_t f = populated(); seal_direction(&f, direction); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + unsigned count = 0; read_fault = nth; + CHECK(wl_session_snapshot(f.session, count_only, + &count) == EIO && count == 0); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + read_fault = 0; events_t e = {0}; + CHECK(wl_session_snapshot(f.session, observe, + &e) == 0 && e.count == 4); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + } +} +static void cold_left_missing_right(void) +{ + for (unsigned cold = 0; cold < 2; cold++) { + fixture_t f = fixture(query, 4*1024*1024, 1024*1024); + int64_t row[] = {7, 8}; + CHECK(wl_session_insert(f.session, "a", row, 1, 2) == 0); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(session_remove_rel(s, "b") == 0); + CHECK(!session_find_rel(s, "b")); + wl_plan_relation_t *rp = join_body(&f, "result"); + /* Exercise absent-right typing directly, including an out-of-left column. */ + wl_plan_op_t *op = (wl_plan_op_t *)&rp->ops[1]; + uint32_t indices[] = {1, 2, 1}; + const uint32_t *saved = op->project_indices; + uint32_t count = op->project_count; + op->project_indices = indices; op->project_count = 3; + if (cold) CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == OK); + eval_stack_t stack; eval_stack_init(&stack); + if (cold) CHECK(wl_columnar_join_cold_left(s, rp, &stack) == 0); + else { + CHECK(col_op_variable(&rp->ops[0], &stack, s) == 0); + CHECK(wl_columnar_join_op(op, &stack, s) == 0); + } + eval_entry_t e; CHECK(eval_stack_pop_relation(&stack, &e) == 0); + CHECK(e.rel->nrows == 0 && e.rel->ncols == 3); + for (unsigned i = 0; i < 3; + i++) CHECK(e.rel->column_types[i] == WIRELOG_TYPE_INT64); + CHECK(eval_stack_dispose_entry(&stack, &e) == 0); + op->project_indices = saved; op->project_count = count; + destroy(&f); + } +} +static void cold_join_preflight(void) +{ + for (unsigned side = 0; side < 2; side++) + for (unsigned kind = 0; kind < 7; kind++) { + fixture_t f = populated(); + wl_col_session_t *s = COL_SESSION(f.session); + wl_plan_relation_t *rp = join_body(&f, "result"); + wl_plan_op_t *op = (wl_plan_op_t *)&rp->ops[1]; + wl_plan_op_t saved = *op; + col_rel_t *a = session_find_rel(s, "a"), + *b = session_find_rel(s, "b"); + + wirelog_column_type_t *old_a = a->column_types, + *old_b = b->column_types; + wirelog_column_type_t bad_a[] = {WIRELOG_TYPE_FLOAT, + WIRELOG_TYPE_INT64}; + wirelog_column_type_t bad_b[] = {WIRELOG_TYPE_UINT64, + WIRELOG_TYPE_INT64}; + uint32_t bad = 99; + if (kind == 0) op->key_count = 0; + if (kind == 1) op->key_count = 2; + if (kind == 2) op->right_filter_expr.size = 1; + if (kind == 3) { + op->project_count = 1; op->project_indices = &bad; + } + if (kind == 4) a->column_types = bad_a; + if (kind == 5) b->column_types = bad_b; + if (kind == 6) op->op = WL_PLAN_OP_FILTER; + uint64_t before = reserved(&f); + CHECK(wl_columnar_session_seal_input(f.session, side ? "b" : "a", + NULL, NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + CHECK(!s->cold_relation && reserved(&f) == before && + session_find_rel(s, "a") == a && session_find_rel(s, "b") == b); + *op = saved; + a->column_types = old_a; + b->column_types = old_b; + seal_direction(&f, 3); events_t e = {0}; + CHECK(wl_session_snapshot(f.session, observe, + &e) == 0 && e.count == 4); + destroy(&f); + } +} + +static fixture_t directional_budget_fixture(uint64_t allowance, bool both) +{ + fixture_t f = fixture(query, allowance, 8*1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + s->spill_join_batch_bytes = 1024; s->spill_join_index_bytes = 64*1024; + for (int64_t i = 0; i < 4096; i++) { + int64_t a[] = {i < 128 ? 0 : 10000+i, i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + } + for (int64_t i = 0; i < (both ? 4096 : 128); i++) { + int64_t b[] = {i < 128 ? 0 : 20000+i, 100+i}; + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + } + return f; +} +typedef struct { unsigned count; uint64_t seen[256]; } reclaimed_events_t; +static void reclaimed_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + reclaimed_events_t *e = context; + CHECK(!strcmp(name, + "result") && width == 3 && row[0] == 0 && row[1] >= 0 && row[1] < 128 && + row[2] >= 100 && row[2] < 228); + unsigned pos = (unsigned)(row[1]*128 + row[2]-100); + uint64_t bit = UINT64_C(1) << (pos%64); + CHECK(!(e->seen[pos/64]&bit)); e->seen[pos/64] |= bit; e->count++; +} +static void directional_reclamation(void) +{ + wl_columnar_memory_governor_test_admitted = budget_admitted; + for (unsigned both = 0; both < 2; both++) { + budget_peak = 0; + fixture_t calibration = directional_budget_fixture(8*1024*1024, + both != 0); + seal_direction(&calibration, both ? 3 : 1); + uint64_t preparation = budget_peak; + reclaimed_events_t e = {0}; + CHECK(wl_session_snapshot(calibration.session, reclaimed_observe, + &e) == 0 && e.count == 16384); + uint64_t allowance = budget_peak; + CHECK(allowance > preparation); + destroy(&calibration); + for (unsigned retain = 0; retain < 2; retain++) { + budget_peak = 0; + fixture_t f = directional_budget_fixture(allowance, both != 0); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f); + wl_columnar_session_test_retain_cold_source = retain != 0; + seal_direction(&f, both ? 3 : 1); + wl_columnar_session_test_retain_cold_source = false; + CHECK(retain ? reserved(&f) > before : reserved(&f) < before); + unsigned owners = 0; + for (wl_columnar_session_cold_relation_t *c = s->cold_relation; c; + c = c->next) { + CHECK((c->retained_source != NULL) == (retain != 0)); owners++; + } + CHECK(owners == (both ? 2u : 1u)); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + e = (reclaimed_events_t){0}; + int rc = wl_session_snapshot(f.session, reclaimed_observe, &e); + CHECK(budget_peak <= allowance); + if (retain) CHECK(rc == ENOSPC && + wl_session_budget_denied(f.session) && e.count == 0); + else CHECK(rc == 0 && e.count == 16384); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + CHECK(!wl_columnar_session_test_has_retained_cold_source(NULL)); + } + } + wl_columnar_memory_governor_test_admitted = NULL; +} +static uint64_t constructor_peak; +static void constructor_result(wl_columnar_spill_status_t status) +{ + CHECK(status == OK); constructor_peak = budget_peak; +} +static void directional_bounded_scratch(void) +{ + uint64_t peaks[2] = {0}; + for (unsigned large = 0; large < 2; large++) { + fixture_t f = fixture(query, 4*1024*1024, 1024*1024); + for (int64_t i = 0; i < (large ? 512 : 64); i++) { + int64_t a[] = {i, 10+i}, b[] = {10000+i, 100+i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + } + seal_direction(&f, 3); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + budget_peak = before; constructor_peak = 0; + wl_columnar_memory_governor_test_admitted = budget_admitted; + wl_columnar_join_test_spill_producer_result = constructor_result; + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_join_cold_left(s, join_body(&f, "result"), + &stack) == 0); + peaks[large] = constructor_peak-before; + CHECK(constructor_peak > before && budget_peak == constructor_peak); + eval_entry_t e; + CHECK(eval_stack_pop_relation(&stack, &e) == 0 && e.rel->nrows == 0); + CHECK(eval_stack_dispose_entry(&stack, + &e) == 0 && reserved(&f) == before); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + wl_columnar_memory_governor_test_admitted = NULL; + wl_columnar_join_test_spill_producer_result = NULL; + destroy(&f); + } + CHECK(peaks[0] == peaks[1]); + /* Distinct resident output floor, after both inputs have been reclaimed. */ + fixture_t f = floor_fixture(256*1024, 128); + seal_direction(&f, 3); wl_col_session_t *s = COL_SESSION(f.session); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + unsigned count = 0; + CHECK(wl_session_snapshot(f.session, count_only, + &count) == ENOSPC && count == 0 && wl_session_budget_denied(f.session)); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); +} + +static void directional_constructor_floor(void) +{ + fixture_t f = populated(); seal_direction(&f, 3); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + budget_peak = before; constructor_peak = 0; + wl_columnar_memory_governor_test_admitted = budget_admitted; + wl_columnar_join_test_spill_producer_result = constructor_result; + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_join_cold_left(s, join_body(&f, "result"), &stack) == 0); + uint64_t scratch = constructor_peak-before; + eval_entry_t e; CHECK(eval_stack_pop_relation(&stack, &e) == 0); + CHECK(eval_stack_dispose_entry(&stack, &e) == 0 && reserved(&f) == before); + wl_columnar_memory_governor_test_admitted = NULL; + wl_columnar_join_test_spill_producer_result = producer_result; + /* A real admitted allocation leaves one byte less than constructor demand. */ + uint64_t pressure_bytes = 4*1024*1024-before-scratch+1; + wl_columnar_memory_reservation_t pressure; + wl_columnar_memory_reservation_init(&pressure); + CHECK(wl_columnar_memory_reserve(wl_columnar_memory_governor_ref_get( + f.governor), pressure_bytes, &pressure)); + void *allocation = malloc((size_t)pressure_bytes); CHECK(allocation); + memset(allocation, 1, (size_t)pressure_bytes); + CHECK(wl_columnar_memory_commit(&pressure, allocation)); + producer_calls = 0; producer_status = OK; + CHECK(wl_columnar_join_cold_left(s, join_body(&f, "result"), + &stack) == ENOSPC); + CHECK(stack.top == 0 && producer_calls == 1 && + producer_status == WL_COLUMNAR_SPILL_MEMORY); + CHECK(reserved(&f) == before+pressure_bytes && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + free(allocation); CHECK(wl_columnar_memory_release(&pressure)); + wl_columnar_join_test_spill_producer_result = NULL; + CHECK(wl_columnar_join_cold_left(s, join_body(&f, "result"), &stack) == 0); + CHECK(eval_stack_pop_relation(&stack, &e) == 0 && e.rel->nrows == 4); + CHECK(eval_stack_dispose_entry(&stack, &e) == 0 && reserved(&f) == before); + destroy(&f); +} + +static void cold_intermediate_floor(void) +{ + const char *text = + ".decl a(k:int64,v:int64)\n.decl b(k:int64,v:int64)\n.decl c(k:int64,v:int64)\n" + ".decl result(k:int64,x:int64,y:int64,z:int64)\nresult(k,x,y,z+0) :- a(k,x),b(k,y),c(y,z).\n"; + for (unsigned large = 0; large < 2; large++) { + fixture_t f = fixture(text, 256*1024, 1024*1024); + for (int64_t i = 0; i < (large ? 128 : 4); i++) { + int64_t a[] = {0, i}, b[] = {0, 100+i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 2) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + } + int64_t c[] = {100000, 0}; + CHECK(wl_session_insert(f.session, "c", c, 1, 2) == 0); + seal_direction(&f, 3); + CHECK(wl_columnar_session_seal_input(f.session, "c", NULL, NULL) == OK); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store), + before = reserved(&f); + wl_plan_relation_t *rp = join_body(&f, "result"); + CHECK(rp->ops[1].op == WL_PLAN_OP_JOIN && + rp->ops[2].op == WL_PLAN_OP_JOIN); + eval_stack_t stack; eval_stack_init(&stack); + int rc = wl_columnar_join_cold_left(s, rp, &stack); + if (large) CHECK(rc == ENOSPC && stack.top == 0); + else { + eval_entry_t e; + CHECK(rc == 0 && eval_stack_pop_relation(&stack, + &e) == 0 && e.rel->nrows == 16); + CHECK(eval_stack_dispose_entry(&stack, &e) == 0); + } + CHECK(reserved(&f) == before); + unsigned count = 0; + rc = wl_session_snapshot(f.session, count_only, &count); + CHECK(rc == (large ? ENOSPC : 0) && count == 0); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + } +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -1614,6 +2093,11 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + cold_intermediate_floor(); + directional_constructor_floor(); + directional_reclamation(); directional_bounded_scratch(); + direction_matrix(); self_join(); chained_consumers(); + directional_failures(); cold_left_missing_right(); cold_join_preflight(); multiple_runtime_guard(); multiple_memory_floor(); multiple_idb_filter_refusal(); multiple_schema_audit(); multiple_right_joins(); diff --git a/tests/test_wirelog_advanced_spill.c b/tests/test_wirelog_advanced_spill.c index cfd78125f..2d423a2a9 100644 --- a/tests/test_wirelog_advanced_spill.c +++ b/tests/test_wirelog_advanced_spill.c @@ -682,6 +682,87 @@ static void sealed_sets(void) } } +static void join_directions(void) +{ + for (unsigned empty = 0; empty < 2; empty++) + for (unsigned direction = 0; direction < 5; direction++) { + wirelog_program_t *p = parse(query); + wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, + WIRELOG_BACKEND_DEFAULT, 1, &o, &s) == WIRELOG_OK); + populate(s, empty != 0); + if (direction == 4) CHECK(wirelog_session_seal_input(s, + "b") == WIRELOG_OK); + if (direction == 1 || + direction >= 3) CHECK(wirelog_session_seal_input(s, + "a") == WIRELOG_OK); + if (direction == 2 || + direction == 3) CHECK(wirelog_session_seal_join_input(s, + "b") == WIRELOG_OK); + if (direction) check_mutation(s); + if (direction == 4) CHECK(wirelog_session_step(s) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + events_t e = {0}; + CHECK(wirelog_session_snapshot(s, observe, &e) == WIRELOG_OK); + CHECK(e.count == (empty ? 0u : 4u) && + e.seen == (empty ? 0u : 15u)); + } + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} +typedef struct { unsigned seen[3]; } join_chain_events_t; +static void join_chain_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + join_chain_events_t *e = context; + unsigned idx = !strcmp(name, "result") ? 0u : !strcmp(name, + "scan") ? 1u : 2u; + CHECK(idx != 2 || !strcmp(name, "second")); + CHECK(width == (idx == 0 ? 4u : idx == 1 ? 1u : 3u) && row[0] >= 0 && + row[0] < 8); + if (idx != 1) CHECK(row[1] == row[0]+10 && row[2] == row[0]+100); + if (idx == 0) CHECK(row[3] == row[0]+1000); + unsigned bit = 1u << (unsigned)row[0]; CHECK(!(e->seen[idx]&bit)); + e->seen[idx] |= bit; +} +static void join_chains(void) +{ + const char *text = + ".decl a(k:int64,v:int64)\n.decl b(k:int64,v:int64)\n.decl c(k:int64,v:int64)\n" + ".decl result(k:int64,x:int64,y:int64,z:int64)\nresult(k,x,y,z+0) :- a(k,x),b(k,y),c(y,z).\n" + ".decl scan(k:int64)\nscan(k) :- a(k,x).\n" + ".decl second(k:int64,x:int64,y:int64)\nsecond(k,x,y+0) :- a(k,x),b(k,y).\n"; + for (unsigned order = 0; order < 3; order++) { + wirelog_program_t *p = parse(text); wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, WIRELOG_BACKEND_DEFAULT, 1, + &o, &s) == WIRELOG_OK); + for (int64_t i = 0; i < 8; i++) { + int64_t a[] = {i, 10+i}, b[] = {i, 100+i}, c[] = {100+i, 1000+i}; + CHECK(wirelog_session_insert(s, "a", a, 1, 2) == WIRELOG_OK); + CHECK(wirelog_session_insert(s, "b", b, 1, 2) == WIRELOG_OK); + CHECK(wirelog_session_insert(s, "c", c, 1, 2) == WIRELOG_OK); + } + if (order) { + const char *names[] = {"a", "b", "c"}; + for (unsigned i = 0; i < 3; + i++) CHECK(wirelog_session_seal_input(s, + names[order == 1 ? i : 2-i]) == WIRELOG_OK); + } + if (order == 2) CHECK(wirelog_session_step(s) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + join_chain_events_t e = {0}; + CHECK(wirelog_session_snapshot(s, join_chain_observe, + &e) == WIRELOG_OK); + CHECK(e.seen[0] == 255 && e.seen[1] == 255 && e.seen[2] == 255); + } + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + int main(void) { /* Issue #2287: a session created with a nonzero WIRELOG_JOIN_BATCH_BYTES @@ -700,6 +781,7 @@ int main(void) test_tmpdir()); CHECK(mkdtemp(directory)); #endif + join_directions(); join_chains(); descriptors(); lifecycle(); refusals(); typed_projection(); inline_facts(); sealed_sets(); standalone_reads(); output_floor(); mixed_projection(); unsupported_filters(); diff --git a/wirelog/columnar/eval_plan.c b/wirelog/columnar/eval_plan.c index 8f1367106..356ba84c3 100644 --- a/wirelog/columnar/eval_plan.c +++ b/wirelog/columnar/eval_plan.c @@ -173,9 +173,9 @@ session_has_delta_rows(void *ctx, const char *relation_name) */ static int wl_columnar_eval_plan_body(const wl_plan_relation_t *rplan, eval_stack_t *stack, - wl_col_session_t *sess) + wl_col_session_t *sess, uint32_t start) { - for (uint32_t i = 0; i < rplan->op_count; i++) { + for (uint32_t i = start; i < rplan->op_count; i++) { const wl_plan_op_t *op = &rplan->ops[i]; if (sess->tdd_input_run) sess->tdd_input_run->current_op = op; @@ -470,8 +470,13 @@ col_eval_relation_plan(const wl_plan_relation_t *rplan, eval_stack_t *stack, wl_columnar_session_cold_find( sess, rplan->ops[i].relation_name); if (!cold) continue; - if (wl_columnar_session_cold_execution_check(sess) || - !wl_columnar_eval_plan_cold_projection(rplan, cold->name, + if (wl_columnar_session_cold_execution_check(sess)) return ENOTSUP; + if (i == 0 && wl_columnar_join_cold_prefix(sess, rplan)) { + int rc = wl_columnar_join_cold_left(sess, rplan, stack); + return rc ? rc : wl_columnar_eval_plan_body(rplan, stack, sess, + 2); + } + if (!wl_columnar_eval_plan_cold_projection(rplan, cold->name, cold->ncols, wl_columnar_batch_payload_schema( cold->payload, NULL, NULL))) return ENOTSUP; int rc = wl_columnar_ops_cold_projection(rplan, stack, sess); @@ -479,13 +484,13 @@ col_eval_relation_plan(const wl_plan_relation_t *rplan, eval_stack_t *stack, } } if (!sess->tdd_input_run) - return wl_columnar_eval_plan_body(rplan, stack, sess); + return wl_columnar_eval_plan_body(rplan, stack, sess, 0); wl_columnar_eval_tdd_run_t *run = sess->tdd_input_run; if (!run->evaluating || run->current_op || rplan->ops != run->manifest->owner_ops + run->slice->start || rplan->op_count != run->slice->count) return EBUSY; - int rc = wl_columnar_eval_plan_body(rplan, stack, sess); + int rc = wl_columnar_eval_plan_body(rplan, stack, sess, 0); run->current_op = NULL; return rc; } diff --git a/wirelog/columnar/internal.h b/wirelog/columnar/internal.h index d92410be9..fb2dd2316 100644 --- a/wirelog/columnar/internal.h +++ b/wirelog/columnar/internal.h @@ -5483,6 +5483,12 @@ col_op_map_output_types(const wl_plan_op_t *op, const col_rel_t *input, int wl_columnar_filter_op(const wl_plan_op_t *op, eval_stack_t *stack, wl_col_session_t *sess); +bool wl_columnar_join_cold_prefix(const wl_col_session_t *s, + const wl_plan_relation_t *rp); +int wl_columnar_join_cold_left(wl_col_session_t *s, + const wl_plan_relation_t *rp, + eval_stack_t *stack); + int wl_columnar_join_op(const wl_plan_op_t *op, eval_stack_t *stack, wl_col_session_t *sess); diff --git a/wirelog/columnar/join.c b/wirelog/columnar/join.c index fdae12a10..4bd085a6d 100644 --- a/wirelog/columnar/join.c +++ b/wirelog/columnar/join.c @@ -1490,44 +1490,171 @@ wl_columnar_join_spill_run(wl_col_session_t *sess, const wl_plan_op_t *op, return rc; } -/* Persistent right owner is session-owned; this attempt owns only left/output. - * No synthetic col_rel with missing columns is passed to resident readers. */ +/* Source metadata is borrowed only until capture/take. Persistent payloads are +* session-owned; a temporary resident snapshot has a separate attempt owner. */ +typedef struct { + const wl_columnar_session_cold_relation_t *cold; + col_rel_t *resident; + uint32_t width; + const char *const *names; + const wirelog_column_type_t *types; +} wl_columnar_join_source_t; + +static wl_columnar_join_source_t +wl_columnar_join_source(const wl_col_session_t *s, const char *name, + col_rel_t *resident) +{ + wl_columnar_join_source_t src = {0}; + src.cold = wl_columnar_session_cold_find(s, name); + if (src.cold) { + src.width = src.cold->ncols; + src.names = (const char *const *)src.cold->col_names; + src.types = wl_columnar_batch_payload_schema(src.cold->payload, NULL, + NULL); + } else { + /* Read-only lookup also serves seal preflight; never build a hash. */ + if (!resident && name) + for (uint32_t i = 0; i < s->nrels; i++) + if (s->rels[i] && !strcmp(s->rels[i]->name, name)) { + resident = s->rels[i]; break; + } + src.resident = resident; + if (resident) { + src.width = resident->ncols; + src.names = (const char *const *)resident->col_names; + src.types = resident->column_types; + } + } + return src; +} + +static uint32_t +wl_columnar_join_source_key(const wl_columnar_join_source_t *src, + const char *name) +{ + int key = wl_columnar_relation_schema_column_index(src->width, src->names, + name); + return key < 0 ? 0 : (uint32_t)key; +} + +static bool +wl_columnar_join_source_valid(const wl_columnar_join_source_t *src, + uint32_t key) +{ + return src->cold ? key < src->width && src->types[key] == WIRELOG_TYPE_INT64 + : src->resident && wl_columnar_join_spill_schema(src->resident, key); +} + +static bool +wl_columnar_join_cold_shape(const wl_plan_op_t *op, + const wl_columnar_join_source_t *left, + const wl_columnar_join_source_t *right) +{ + if (op->op != WL_PLAN_OP_JOIN || op->key_count != 1 || op->opaque_data || + op->materialized || op->right_filter_expr.size || !op->right_relation || + (op->project_count && !op->project_indices) || + (op->delta_mode != WL_DELTA_AUTO && + op->delta_mode != WL_DELTA_FORCE_FULL)) + return false; + uint32_t lk = wl_columnar_join_source_key(left, + op->left_keys ? op->left_keys[0] : NULL); + uint32_t rk = wl_columnar_join_source_key(right, + op->right_keys ? op->right_keys[0] : NULL); + /* An unloaded left has no runtime schema yet. Preflight can still audit + * the known right; the actual entry is checked again before execution. */ + if (!left->cold && !left->width && + (!left->resident || !left->resident->nrows)) + return (!right->cold && !right->resident) || + wl_columnar_join_source_valid(right, rk); + if (!wl_columnar_join_source_valid(left, lk)) return false; + /* Missing right retains ordinary empty-result/projection typing semantics. */ + if (!right->cold && !right->resident) return true; + if (!wl_columnar_join_source_valid(right, rk)) return false; + uint64_t joined = (uint64_t)left->width + right->width; + if (joined > UINT32_MAX) return false; + for (uint32_t i = 0; i < op->project_count; i++) + if (op->project_indices[i] >= joined) return false; + return true; +} + +/* The same complete prefix contract is used before retirement and at runtime. +* Its first JOIN creates an ordinary resident entry for the audited suffix. */ +bool +wl_columnar_join_cold_prefix(const wl_col_session_t *s, + const wl_plan_relation_t *rp) +{ + if (!rp || !rp->ops || rp->op_count < 3 || + rp->ops[0].op != WL_PLAN_OP_VARIABLE || !rp->ops[0].relation_name || + rp->ops[rp->op_count - 1].op != WL_PLAN_OP_CONSOLIDATE) return false; + for (uint32_t i = 0; i < rp->op_count; i++) { + const wl_plan_op_t *op = &rp->ops[i]; + if (op->opaque_data || op->materialized || + (op->delta_mode != WL_DELTA_AUTO && + op->delta_mode != WL_DELTA_FORCE_FULL) || + (i && op->relation_name) || (i && op->op != WL_PLAN_OP_JOIN && + op->op != WL_PLAN_OP_MAP && + op->op != WL_PLAN_OP_CONSOLIDATE)) return false; + } + wl_columnar_join_source_t left = wl_columnar_join_source(s, + rp->ops[0].relation_name, NULL); + wl_columnar_join_source_t right = wl_columnar_join_source(s, + rp->ops[1].right_relation, NULL); + if (!wl_columnar_join_cold_shape(&rp->ops[1], &left, &right)) return false; + uint64_t width = left.width; + for (uint32_t i = 1; i < rp->op_count; i++) { + const wl_plan_op_t *op = &rp->ops[i]; + if (op->op == WL_PLAN_OP_MAP) { + width = op->project_count; + } else if (op->op == WL_PLAN_OP_JOIN) { + if (op->key_count != 1 || !op->right_relation || + op->right_filter_expr.size || + (op->project_count && !op->project_indices)) return false; + wl_columnar_join_source_t rhs = wl_columnar_join_source(s, + op->right_relation, NULL); + if (rhs.cold || rhs.resident) { + uint32_t key = wl_columnar_join_source_key(&rhs, + op->right_keys ? op->right_keys[0] : NULL); + if (!wl_columnar_join_source_valid(&rhs, key)) return false; + if (width && width + rhs.width > UINT32_MAX) return false; + for (uint32_t col = 0; width && col < op->project_count; col++) + if (op->project_indices[col] >= + width + rhs.width) return false; + if (width) width += rhs.width; + } + if (op->project_count) width = op->project_count; + } + } + return true; +} + static int wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, - eval_stack_t *stack, eval_entry_t *left) + eval_stack_t *stack, eval_entry_t *left, const char *cold_left) { #ifdef WL_PROFILE uint64_t started = now_ns(); sess->profile.join_calls++; #endif - const wl_columnar_session_cold_relation_t *cold = - wl_columnar_session_cold_find(sess, op->right_relation); + wl_columnar_join_source_t sources[2] = { + wl_columnar_join_source(sess, cold_left, left->rel), + wl_columnar_join_source(sess, op->right_relation, NULL) + }; int rc = wl_columnar_session_cold_execution_check(sess); - if (rc || op->key_count != 1 || op->materialized || - op->right_filter_expr.size || left->is_delta || - (op->delta_mode != WL_DELTA_AUTO && - op->delta_mode != WL_DELTA_FORCE_FULL)) + if (rc || left->is_delta || !wl_columnar_join_cold_shape(op, &sources[0], + &sources[1])) return wl_columnar_join_dispose_left(stack, left, rc ? rc : ENOTSUP); - uint32_t lk = col_op_resolve_key(left->rel, - op->left_keys ? op->left_keys[0] : NULL, "left", "JOIN", - op->right_relation); - int resolved = wl_columnar_relation_schema_column_index(cold->ncols, - (const char *const *)cold->col_names, + uint32_t lk = wl_columnar_join_source_key(&sources[0], + op->left_keys ? op->left_keys[0] : NULL); + uint32_t rk = wl_columnar_join_source_key(&sources[1], op->right_keys ? op->right_keys[0] : NULL); - uint32_t rk = resolved < 0 ? 0 : (uint32_t)resolved; - const wirelog_column_type_t *right_types = wl_columnar_batch_payload_schema( - cold->payload, NULL, NULL); - if (!wl_columnar_join_spill_schema(left->rel, lk) || - rk >= cold->ncols || right_types[rk] != WIRELOG_TYPE_INT64) + /* Preflight permits an unloaded zero-width resident placeholder; runtime + * must still have the real supported left schema before creating readers. */ + if (!wl_columnar_join_source_valid(&sources[0], lk)) return wl_columnar_join_dispose_left(stack, left, ENOTSUP); - uint64_t joined = (uint64_t)left->rel->ncols + cold->ncols; + bool missing_right = !sources[1].cold && !sources[1].resident; + uint32_t joined = sources[0].width + (missing_right ? 0 : sources[1].width); bool projected = op->project_count && op->project_indices; - if (joined > UINT32_MAX || (op->project_count && !op->project_indices)) - return wl_columnar_join_dispose_left(stack, left, EINVAL); - uint32_t width = projected ? op->project_count : (uint32_t)joined; - for (uint32_t i = 0; projected && i < width; i++) - if (op->project_indices[i] >= joined) - return wl_columnar_join_dispose_left(stack, left, EINVAL); + uint32_t width = projected ? op->project_count : joined; uint64_t type_bytes = (uint64_t)width * sizeof(wirelog_column_type_t); if (type_bytes > SIZE_MAX) return wl_columnar_join_dispose_left(stack, left, EOVERFLOW); @@ -1543,34 +1670,46 @@ wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, WL_COLUMNAR_SPILL_MEMORY : WL_COLUMNAR_SPILL_OVERFLOW)); wirelog_column_type_t *types = malloc((size_t)type_bytes); col_rel_t *out = NULL; - wl_columnar_batch_payload_t *input = NULL; + wl_columnar_batch_payload_t *inputs[2] = {NULL, NULL}; + bool owned[2] = {false, false}; wl_columnar_continuation_t *cont = NULL; if (!types) rc = ENOMEM; for (uint32_t i = 0; !rc && i < width; i++) { uint32_t source = projected ? op->project_indices[i] : i; - types[i] = source < left->rel->ncols ? - (left->rel->column_types ? left->rel->column_types[source] : - WIRELOG_TYPE_INT64) : right_types[source - left->rel->ncols]; + unsigned side = source < sources[0].width ? 0u : 1u; + uint32_t col = side ? source - sources[0].width : source; + types[i] = sources[side].types && col < sources[side].width ? + sources[side].types[col] : WIRELOG_TYPE_INT64; } - if (!rc) rc = wl_columnar_join_new_output(sess, left->rel, NULL, - "$join", width, 0, false, true, &out); + if (!rc) rc = wl_columnar_join_new_output(sess, sources[0].resident, + sources[1].resident, "$join", width, 0, false, true, &out); if (!rc) rc = col_rel_set_column_types(out, types, width); free(types); if (!wl_columnar_memory_rollback(&type_token)) abort(); - if (rc) goto done; + if (rc || missing_right) goto done; sess->spill_join_selected_count++; - wl_columnar_spill_status_t status; - if (left->owned && wl_columnar_batch_payload_can_take(left->rel)) - status = wl_columnar_batch_payload_take(sess->memory_governor, - sess->spill_store, &left->rel, NULL, NULL, &input); - else - status = wl_columnar_batch_payload_capture(sess->memory_governor, - sess->spill_store, left->rel, left->rel->nrows, - WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, NULL, NULL, &input); - rc = wl_columnar_join_spill_error(sess, status); - if (rc) goto done; - status = wl_columnar_payload_join_create_projected_bounded( - sess->memory_governor, input, cold->payload, lk, rk, + for (unsigned i = 0; i < 2; i++) { + if (sources[i].cold) { + inputs[i] = sources[i].cold->payload; + continue; + } + wl_columnar_spill_status_t status; + if (i == 0 && left->owned && + wl_columnar_batch_payload_can_take(left->rel)) + status = wl_columnar_batch_payload_take(sess->memory_governor, + sess->spill_store, &left->rel, NULL, NULL, &inputs[i]); + else + status = wl_columnar_batch_payload_capture(sess->memory_governor, + sess->spill_store, sources[i].resident, + sources[i].resident->nrows, + WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, NULL, NULL, &inputs[i]); + owned[i] = inputs[i] != NULL; + rc = wl_columnar_join_spill_error(sess, status); + if (rc) goto done; + } + wl_columnar_spill_status_t status = + wl_columnar_payload_join_create_projected_bounded( + sess->memory_governor, inputs[0], inputs[1], lk, rk, op->project_indices, op->project_count, sess->spill_join_batch_bytes, sess->spill_join_index_bytes, NULL, NULL, &cont); @@ -1581,10 +1720,14 @@ wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, rc = wl_columnar_join_spill_error(sess, status); if (!rc) rc = wl_columnar_join_spill_drain(sess, cont, out); done: + /* Even self-JOIN drops two reader pins before any temporary owner. The + * persistent owner is borrowed on each side and is never released here. */ wl_columnar_continuation_destroy(cont); - if (input && - wl_columnar_batch_payload_release(input) != WL_COLUMNAR_SPILL_OK) - abort(); + for (unsigned i = 0; i < 2; i++) + if (owned[i] && + wl_columnar_batch_payload_release(inputs[i]) != + WL_COLUMNAR_SPILL_OK) + abort(); if (rc) { col_rel_destroy(out); return wl_columnar_join_dispose_left(stack, left, @@ -1597,6 +1740,18 @@ wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, return wl_columnar_join_publish_after_left(stack, left, out, false); } +int +wl_columnar_join_cold_left(wl_col_session_t *sess, const wl_plan_relation_t *rp, + eval_stack_t *stack) +{ + if (!wl_columnar_join_cold_prefix(sess, rp) || + !wl_columnar_session_cold_find(sess, + rp->ops[0].relation_name)) return ENOTSUP; + eval_entry_t left = {0}; + return wl_columnar_join_cold_run(sess, &rp->ops[1], stack, &left, + rp->ops[0].relation_name); +} + int wl_columnar_join_op(const wl_plan_op_t *op, eval_stack_t *stack, wl_col_session_t *sess) @@ -1621,7 +1776,7 @@ wl_columnar_join_op(const wl_plan_op_t *op, eval_stack_t *stack, if (pop_rc != 0) return pop_rc; if (wl_columnar_session_cold_find(sess, op->right_relation)) - return wl_columnar_join_cold_run(sess, op, stack, &left_e); + return wl_columnar_join_cold_run(sess, op, stack, &left_e, NULL); /* right_filtered: non-NULL only when right was pool-allocated by * wl_columnar_filter_apply_right_filter (non-cached path: antijoin/semijoin callers). diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 8a8f83f25..4d7b6a098 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -685,7 +685,10 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, rp); bool selected_projection = projection && !strcmp(rp->ops[0].relation_name, name); - if (selected_projection) consumer = true; + bool selected_join = rp->op_count && rp->ops[0].relation_name && + !strcmp(rp->ops[0].relation_name, name) && + wl_columnar_join_cold_prefix(sess, rp); + if (selected_projection || selected_join) consumer = true; for (uint32_t oi = 0; oi < rp->op_count; oi++) { const wl_plan_op_t *op = &rp->ops[oi]; /* Reject all opaque rewrites, even if top-level names hide @@ -698,7 +701,7 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, (op->delta_mode != WL_DELTA_AUTO && op->delta_mode != WL_DELTA_FORCE_FULL)) return false; if (op->relation_name && strcmp(op->relation_name, name) == 0 && - !selected_projection) + !(selected_projection || (selected_join && oi == 0))) return false; if (!op->right_relation || strcmp(op->right_relation, name) != 0) @@ -711,6 +714,8 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, /* The resident JOIN also falls back to column zero. */ if (key < 0) key = 0; if (types && types[key] != WIRELOG_TYPE_INT64) return false; + if (oi == 1 && rp->ops[0].op == WL_PLAN_OP_VARIABLE && + !wl_columnar_join_cold_prefix(sess, rp)) return false; consumer = true; } } diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index 24de9aa0c..4d8a961fb 100644 --- a/wirelog/wirelog-advanced.h +++ b/wirelog/wirelog-advanced.h @@ -218,14 +218,17 @@ wirelog_session_create_with_options(wirelog_program_t *program, * the first STEP/SNAPSHOT attempt. Load all inputs first. Requires configured * spill JOIN caps and a serial nonrecursive plan using only audited * VARIABLE/JOIN/MAP/CONSOLIDATE operations and the restricted FILTER below. - * The selected name must have an eligible consumer: a JOIN right input with - * one INT64 key or a complete + * The selected name must have an eligible consumer: a JOIN input with + * one INT64 equality key or a complete * VARIABLE/MAP/CONSOLIDATE rule selecting only columns, optionally with one * FILTER before MAP comparing an INT64 column with an integer literal * (=, !=, <, >, <=, >=; column on the left). Projected results and consolidation * remain resident; output capacity is admitted for the source row count even * when no row passes. Projection-only programs need no JOIN consumer. - * General cold pipelines remain unsupported. No right filter, timestamps, references, + * A cold left input is consumed by an initial VARIABLE/JOIN pair; both sides + * may be sealed, including a self-JOIN. Subsequent JOINs consume an admitted + * resident intermediate. No FILTER/MAP before that first cold JOIN, multi-key, + * cross/theta JOIN, non-INT64 key, right filter, timestamps, references, * opaque/materialized rewrites, worker/control/delta modes or delta callback. * Optimized plans may therefore be ineligible. The session must also not have * been created with WIRELOG_JOIN_BATCH_BYTES on (see From 689081d85ca70a969d02d2bf16e45fdae364e26c Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 18:18:22 +0900 Subject: [PATCH 4/9] feat(columnar): consume sealed STRING projections Pin and audit originating intern provenance for standalone cold projections. Preserve declared STRING input types and fixed builtin MAP result schemas so ordinary JOIN pipelines retain their key metadata. Cover public consumers, malformed bytecode, ownership, failures and resident memory floors. Refs #1372 --- CHANGELOG.md | 5 + docs/MEMORY.md | 33 +++- docs/SEMANTICS.md | 22 ++- tests/test_batch_payload.c | 8 + tests/test_doop_strings.c | 7 + tests/test_expr_compile.c | 281 ++++++++++++++++++++++++++++ tests/test_session_cold_relation.c | 220 +++++++++++++++++++++- tests/test_string_eval.c | 35 ++++ tests/test_wirelog_advanced.c | 115 ++++++++++-- tests/test_wirelog_advanced_spill.c | 75 ++++++++ wirelog/columnar/batch_payload.c | 9 + wirelog/columnar/batch_payload.h | 6 + wirelog/columnar/eval_plan.c | 7 +- wirelog/columnar/expr.c | 148 +++++++++++++++ wirelog/columnar/internal.h | 6 + wirelog/columnar/ops.c | 6 +- wirelog/columnar/session.c | 39 ++-- wirelog/columnar/spill.c | 8 + wirelog/columnar/spill.h | 5 + wirelog/wirelog-advanced.h | 8 +- 20 files changed, 1006 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7259b1402..e3cc1bb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to wirelog are documented in this file. ### Added +- **STRING cold projections** (#1372): standalone direct/filtered projections + consume sealed STRING lanes with actual types, originating-intern checks and + pinned storage. Multiple bindings, failure/retry and fixed-budget tests retain + explicit resident intern/output floors; reference JOINs remain follow-up work. + - **Sealed JOIN inputs in either position** (#1372): bounded readers now consume cold left, both-cold and self-JOIN inputs. Audited chains continue through admitted resident intermediates, preserving single-INT64-key semantics, diff --git a/docs/MEMORY.md b/docs/MEMORY.md index a3d372b6b..d3ab298df 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -3604,8 +3604,12 @@ parser used by the integer FILTER shortcut. Raw MAP reserves its temporary native type vector in the existing scratch admission, keeps full governed row-capacity admission before callbacks, and uses the existing metadata setter only on the empty private output. A raw -interpreter-only expression lane retains the legacy untyped schema for the -whole output; mixed numeric/addon/string fallback results are not covered by +unclassified interpreter expression lane retains the legacy untyped schema for +the whole output. Existing fixed-result scalar builtins use a pure complete- +buffer/depth/final-producer check with constant scratch; they preserve complete +result metadata without execution, crypto, interning or callbacks during schema +resolution. Runtime value validation and evaluation errors remain separate. +Opaque mixed numeric/addon fallback results are not covered by the numeric FLOAT ordering guarantee. The temporary vector is freed before row evaluation. Pool/arena output ownership remains available without a governor. @@ -4980,9 +4984,30 @@ credit. The persistent binding stays session-owned and valid for permitted retries. This does not promise rollback of other heads already committed by an earlier STEP relation, or add asynchronous cancellation/control support. +### STRING cold projection consumers (#1372, partial) + +Standalone direct and narrowly filtered projections accept actual STRING lanes, +including reordered and repeated outputs and multiple sealed inputs. Source +initialization preserves declared STRING types even for raw host inserts and +empty relations. Capture validates every STRING ID, including unused lanes, +and pins the originating session/program intern table. Runtime checks compare +table identity through the retained payload or spill-segment owner; a different +table with coincident IDs is not equivalent. Existing numeric JOIN routes remain +numeric-only in this increment; reference-bearing JOINs are refused. + +Projection preserves STRING types and IDs without rehydrating full input rows. +The program must outlive public sessions; output IDs borrow that lifetime. +Payload readers retain source storage while live, including after owner +retirement. Independently retained output requires its own intern capability. +Intern strings/table storage remain resident and charged. Tests pair reclaimed +and retained STRING sources under one allowance and separately exhaust real +admission with increasing distinct strings; spilling rows cannot remove this +floor. Private output/read/allocation failures preserve the sealed binding and +release attempt resources before retry. + ### Remaining #1372 implementation groups -The sealed numeric JOIN-input subset now has actual persistent cold storage, +The sealed numeric JOIN and scalar STRING projection subsets now have actual persistent cold storage, resident reclamation and evaluator consumers. General cold VARIABLE/direct-column access beyond the exact direct-projection subset, mutable relations, persistent trace/differential storage and their arrangement consumers remain separate integration work. They require stable @@ -4991,7 +5016,7 @@ and retractions. Unsupported consumers remain explicitly ineligible for sealing. SIDE/nested-reference ownership also needs a concrete provenance decision. Public advanced configuration and explicit sealing are now available for the -numeric subset above. Broader consumer support and native platform qualification +subsets above. Broader consumer support and native platform qualification remain evidence-dependent. Existing payload and sealed-session evidence cannot replace those acceptance requirements. Close #1372 only when the remaining gaps have concrete tests. diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index 886302fdb..3a87dd609 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -250,7 +250,7 @@ caps. NULL preserves legacy defaults; existing creators are unchanged. Inline facts still load before return. Options and directory storage are borrowed only for construction; the program stays borrowed for the session lifetime. -`wirelog_session_seal_input` exposes the internal pre-evaluation numeric-input +`wirelog_session_seal_input` exposes the internal pre-evaluation eligible-input transition; `wirelog_session_seal_join_input` is a compatibility spelling with identical behavior. A sealed JOIN input may additionally feed an exact `VARIABLE → MAP → CONSOLIDATE` direct column-projection rule. Column indices @@ -269,7 +269,7 @@ It retains the conservative serial immutable-plan eligibility checks and makes all fact/compound mutation unsupported after success. Additional already-loaded eligible inputs may be sealed independently before evaluation; failure preserves prior bindings and the failed resident -source. Complete narrow projections over other resident numeric EDBs coexist +source. Complete narrow projections over other resident scalar EDBs coexist using their own schema checks. Refusal cannot reopen the seal window after an evaluation attempt. Cold lookup never substitutes empty input for an unsupported consumer. Every session operation remains externally serialized. @@ -304,6 +304,14 @@ easy facade -- the easy surface hides backend selection by design. The asymmetry is recorded here so future maintainers do not assume symmetric coverage. +Standalone direct/filtered cold projections also accept STRING lanes from the +originating program's intern table, preserving STRING output types and IDs. +Every STRING lane is validated at capture, including unprojected lanes. Empty +strings and non-ASCII strings retain their ordinary intern identity. Reference +JOINs and compound sources remain unsupported. Pins retain backing storage; the +public program must still outlive its sessions, and intern storage is a resident +memory floor. A copied raw ID alone does not extend that lifetime. + ### References - `wirelog/wirelog-easy.c` — `ensure_plan_built` performs the seed. @@ -1229,7 +1237,15 @@ BOOL, and integer arithmetic produces INT64. The cached producer follows the existing permissive expression stack rules: ignored full-stack pushes and aggregate no-ops do not replace the actual result producer. -Raw interpreter-only expression families retain the legacy whole-output +Well-formed compositions of existing fixed-result scalar builtins also have +static result types, including string literals, conversions, string functions, +comparisons and numeric digest/UUID functions. UUID5_RFC produces STRING; +numeric UUID functions produce INT64. Classification checks the complete buffer, +operand depth and final producer without evaluation, interning, crypto or +callbacks. It preserves direct STRING lanes beside interpreted builtin results +through ordinary JOIN/SEMIJOIN, including empty MAP inputs. Classification does +not validate runtime values or guarantee successful evaluation. +Opaque extension expressions retain the legacy whole-output untyped schema when any lane is unresolved. This includes mixed numeric/addon results; they do not acquire a fabricated partial schema. Numeric schema resolution invokes no callbacks. Existing extension invocation/admission rules diff --git a/tests/test_batch_payload.c b/tests/test_batch_payload.c index 32126273a..a7acd2138 100644 --- a/tests/test_batch_payload.c +++ b/tests/test_batch_payload.c @@ -532,6 +532,13 @@ string_payloads(void) uint32_t cols, rows; CHECK(wl_columnar_batch_payload_schema(p, &cols, &rows)[0] == WIRELOG_TYPE_STRING && cols == 3 && rows == 8); + CHECK(wl_columnar_batch_payload_uses_intern(p, intern)); + CHECK(!wl_columnar_batch_payload_uses_intern(p, NULL)); + wl_intern_t *foreign = wl_intern_create(); CHECK(foreign); + CHECK(wl_intern_attach_memory_governor(foreign, g) == 0); + CHECK(wl_intern_put(foreign, "different-at-id-zero") == 0); + CHECK(!wl_columnar_batch_payload_uses_intern(p, foreign)); + wl_intern_free(foreign); CHECK(wl_intern_detach_memory_governor(intern, g) == EBUSY); for (unsigned i = 0; i < 1024; i++) { char value[32]; snprintf(value, sizeof(value), "growth-%u", i); @@ -556,6 +563,7 @@ string_payloads(void) &jobs[1]) == 0); if (mode == 1) wl_intern_free(intern); /* Retire while pinned readers run. */ wl_columnar_spill_store_retire(store); + CHECK(wl_columnar_batch_payload_uses_intern(p, intern)); CHECK(wl_columnar_batch_payload_release(p) == WL_COLUMNAR_SPILL_BUSY); wl_thread_join(&threads[0]); wl_thread_join(&threads[1]); const char *bad = "sentinel"; diff --git a/tests/test_doop_strings.c b/tests/test_doop_strings.c index 5a3c1430f..1795cac05 100644 --- a/tests/test_doop_strings.c +++ b/tests/test_doop_strings.c @@ -63,6 +63,7 @@ * rules are structurally identical, so again this pins the set. */ +#include "../wirelog/columnar/internal.h" #include "../wirelog/backend.h" #include "../wirelog/exec_plan_gen.h" #include "../wirelog/passes/fusion.h" @@ -114,6 +115,7 @@ static int fail_count; typedef struct { const char *rel; uint32_t count; + bool string_schema; const wirelog_intern_t *intern; /* Interned values of the row, joined with '|', for the first MAX_SEEN * rows. Counting alone is not enough: swapping one rule constant for @@ -197,6 +199,10 @@ eval_relation(const char *src, const char *relation, collect_t *out) && wl_session_snapshot(sess, collect_cb, out) == 0) result = 0; + col_rel_t *observed = session_find_rel(COL_SESSION(sess), relation); + out->string_schema = observed && observed->ncols && observed->column_types; + for (uint32_t c = 0; out->string_schema && c < observed->ncols; c++) + out->string_schema = observed->column_types[c] == WIRELOG_TYPE_STRING; wl_session_destroy(sess); wl_plan_free(plan); wirelog_program_free(prog); @@ -514,6 +520,7 @@ test_covariant_override_survives_lookup(void) * reading the inherited tuple is shadowed and this is 3. */ ASSERT(c.count == 4, "expected the inherited covariant variant to " "survive; params-only descriptors shadow it"); + ASSERT(c.string_schema, "STRING schema must survive CAT and SEMIJOIN"); ASSERT(saw(&c, "it|OfLong()|Sub|"), "Sub must inherit the covariant OfLong variant from Sup"); PASS(); diff --git a/tests/test_expr_compile.c b/tests/test_expr_compile.c index ec1dd285a..ffa05eaf7 100644 --- a/tests/test_expr_compile.c +++ b/tests/test_expr_compile.c @@ -19,6 +19,8 @@ */ #include "../wirelog/backend.h" +#include "../wirelog/columnar/internal.h" +#include "../wirelog/intern.h" #include "../wirelog/exec_plan_gen.h" #include "../wirelog/passes/fusion.h" #include "../wirelog/passes/jpp.h" @@ -548,12 +550,291 @@ test_float_arithmetic_precedence(void) PASS(); } +static uint32_t string_operand(uint8_t *out, uint8_t tag, const char *text) +{ + uint16_t size = (uint16_t)strlen(text); + out[0] = tag; memcpy(out + 1, &size, 2); memcpy(out + 3, text, size); + return 3u + size; +} +static void test_string_result_classification(void) +{ + TEST("pure complete STRING/CAT classification and malformed refusal"); + wirelog_column_type_t source[] = {WIRELOG_TYPE_STRING, WIRELOG_TYPE_INT64}; + wirelog_column_type_t result = WIRELOG_TYPE_BOOL; + uint8_t expression[2048]; + uint32_t size = string_operand(expression, WL_PLAN_EXPR_VAR, "col0"); + size += string_operand(expression+size, WL_PLAN_EXPR_CONST_STR, "("); + expression[size++] = WL_PLAN_EXPR_STR_FN_CAT; + size += string_operand(expression+size, WL_PLAN_EXPR_CONST_STR, ""); + expression[size++] = WL_PLAN_EXPR_STR_FN_CAT; + if (!wl_columnar_expr_interpreted_result_type(expression, size, source, 2, + &result) || result != WIRELOG_TYPE_STRING){ + FAIL("nested CAT type missing"); return; + } + for (unsigned variant = 0; variant < 13; variant++) { + uint8_t bad[2048]; memcpy(bad, expression, size); uint32_t count = size; + if (variant == 0) bad[count++] = WL_PLAN_EXPR_EXTENSION_CALL; + if (variant == 1) bad[count++] = WL_PLAN_EXPR_CONST_STR; + if (variant == 2) count = 2; /* Truncated length. */ + if (variant == 3) count = 6; /* Truncated column name. */ + if (variant == 4) { + bad[0] = WL_PLAN_EXPR_VAR_STRING; bad[6] = '1'; + } /* Actual INT64 lane. */ + if (variant == 5) bad[6] = '2'; /* Out of range. */ + if (variant == 6) bad[0] = WL_PLAN_EXPR_VAR_FLOAT; + if (variant == 7) { + bad[0] = WL_PLAN_EXPR_STR_FN_CAT; count = 1; + } + if (variant == 8) count += string_operand(bad+count, + WL_PLAN_EXPR_CONST_STR, "extra"); + if (variant == 9) bad[count-1] = WL_PLAN_EXPR_STR_FN_TO_UPPER; + if (variant == 10) count = string_operand(bad, WL_PLAN_EXPR_VAR, + "col4294967296"); + if (variant == 11) count = string_operand(bad, WL_PLAN_EXPR_VAR, + "col00000000000000001"); + if (variant == 12) bad[3] = 'x'; + result = WIRELOG_TYPE_BOOL; + if (wl_columnar_expr_interpreted_result_type(bad, count, source, 2, + &result) || result != WIRELOG_TYPE_BOOL){ + FAIL("malformed/unknown expression classified"); return; + } + } + expression[0] = WL_PLAN_EXPR_VAR_STRING; + if (!wl_columnar_expr_interpreted_result_type(expression, size, source, 2, + &result) || + wl_columnar_expr_interpreted_result_type(expression, size, NULL, 2, + &result)){ + FAIL("explicit STRING reference provenance"); return; + } + for (unsigned extra = 0; extra < 2; extra++) { + size = 0; + for (unsigned i = 0; i < COL_FILTER_STACK + extra; i++) + size += string_operand(expression+size, WL_PLAN_EXPR_CONST_STR, ""); + for (unsigned i = 1; i < COL_FILTER_STACK + extra; + i++) expression[size++] = WL_PLAN_EXPR_STR_FN_CAT; + if (wl_columnar_expr_interpreted_result_type(expression, size, source, + 2, &result) != (extra == 0)){ + FAIL("operand stack bound"); return; + } + } + PASS(); +} + +static void test_builtin_result_contracts(void) +{ + TEST("all fixed builtin result types, arities and opaque refusal"); + const struct { uint8_t tag; unsigned arity; wirelog_column_type_t type; + } cases[] = { + {WL_PLAN_EXPR_ARITH_ADD, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SUB, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_MUL, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_DIV, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_MOD, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_BAND, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_BOR, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_BXOR, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHL, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHR, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_HMAC_SHA256, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_UUID5, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_HMAC_SHA256_SS, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_HMAC_SHA256_SI, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_HMAC_SHA256_IS, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_UUID5_SS, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_UUID5_SI, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_UUID5_IS, 2, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_BNOT, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_HASH, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_CRC32_ETH, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_CRC32_CAST, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_MD5, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHA1, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHA256, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHA512, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_HASH_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_CRC32_ETH_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_CRC32_CAST_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_MD5_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHA1_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHA256_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_SHA512_S, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_STR_FN_STRLEN, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_STR_FN_STR_ORD, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_STR_FN_TO_NUMBER, 1, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_UUID4, 0, WIRELOG_TYPE_INT64}, + {WL_PLAN_EXPR_ARITH_FLOAT_ADD, 2, WIRELOG_TYPE_FLOAT}, + {WL_PLAN_EXPR_ARITH_FLOAT_SUB, 2, WIRELOG_TYPE_FLOAT}, + {WL_PLAN_EXPR_ARITH_FLOAT_MUL, 2, WIRELOG_TYPE_FLOAT}, + {WL_PLAN_EXPR_ARITH_FLOAT_DIV, 2, WIRELOG_TYPE_FLOAT}, + {WL_PLAN_EXPR_CMP_EQ, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_NEQ, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_LT, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_GT, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_LTE, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_GTE, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_FLOAT_EQ, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_FLOAT_NEQ, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_FLOAT_LT, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_FLOAT_GT, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_FLOAT_LTE, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_FLOAT_GTE, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_STR_EQ, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_STR_NEQ, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_STR_LT, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_STR_GT, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_STR_LTE, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_CMP_STR_GTE, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_STR_FN_CONTAINS, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_STR_FN_STR_PREFIX, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_STR_FN_STR_SUFFIX, 2, WIRELOG_TYPE_BOOL}, + {WL_PLAN_EXPR_STR_FN_TO_UPPER, 1, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_TO_LOWER, 1, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_TRIM, 1, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_TO_STRING, 1, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_CAT, 2, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_UUID5_RFC, 2, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_SUBSTR, 3, WIRELOG_TYPE_STRING}, + {WL_PLAN_EXPR_STR_FN_STR_REPLACE, 3, WIRELOG_TYPE_STRING}, + }; + uint8_t expression[1024]; wirelog_column_type_t result; + for (unsigned i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { + uint32_t size = 0; + for (unsigned j = 0; j < cases[i].arity; j++) { + expression[size++] = WL_PLAN_EXPR_CONST_INT; + memset(expression+size, 0, 8); size += 8; + } + expression[size++] = cases[i].tag; + result = WIRELOG_TYPE_UINT64; + if (!wl_columnar_expr_interpreted_result_type(expression, size, NULL, 0, + &result) || result != cases[i].type){ + FAIL("builtin result contract"); return; + } + if (cases[i].arity && + wl_columnar_expr_interpreted_result_type(expression+9, size-9, NULL, + 0, &result)){ + FAIL("operator underflow"); return; + } + /* Every STRING producer must keep a direct key in a zero-row MAP. + * Even crypto tags are classified without invoking execution. */ + if (cases[i].type == WIRELOG_TYPE_STRING) { + col_rel_t *input = col_rel_new_auto("in", 1), + *output = col_rel_new_auto("out", 2); + wl_col_session_t session = {0}; session.intern = wl_intern_create(); + wirelog_column_type_t string_type = WIRELOG_TYPE_STRING; + uint8_t key[32]; + uint32_t key_size = string_operand(key, WL_PLAN_EXPR_VAR, "col0"); + wl_plan_expr_buffer_t exprs[] = {{.data = key, .size = key_size}, + {.data = expression, + .size = size}}; + uint32_t indices[] = {0, 0}; + wl_plan_op_t map = {.op = WL_PLAN_OP_MAP, .project_count = 2, + .project_indices = indices, .map_expr_count = 2, + .map_exprs = exprs}; + bool ok = input && output && session.intern && + col_rel_set_column_types(input, &string_type, 1) == 0 && + col_op_map_output_types(&map, input, &session, NULL, + output) == 0 && output->column_types && + output->column_types[0] == WIRELOG_TYPE_STRING && + output->column_types[1] == WIRELOG_TYPE_STRING && + wl_intern_count(session.intern) == 0 && !output->nrows; + col_rel_destroy(input); col_rel_destroy(output); + wl_intern_free(session.intern); + if (!ok) { + FAIL("empty mixed MAP type or classifier side effect"); return; + } + } + } + const uint8_t opaque[] = {WL_PLAN_EXPR_EXTENSION_CALL, + WL_PLAN_EXPR_EXTENSION_CALL_ABI, + WL_PLAN_EXPR_AGG_COUNT, WL_PLAN_EXPR_AGG_SUM, + WL_PLAN_EXPR_AGG_MIN, WL_PLAN_EXPR_AGG_MAX, + WL_PLAN_EXPR_AGG_AVG, 0xff}; + for (unsigned i = 0; i < sizeof(opaque); i++) { + uint32_t size = string_operand(expression, WL_PLAN_EXPR_CONST_STR, + "uninterned"); + expression[size++] = opaque[i]; + expression[size++] = WL_PLAN_EXPR_STR_FN_TO_UPPER; + result = WIRELOG_TYPE_UINT64; + if (wl_columnar_expr_interpreted_result_type(expression, size, NULL, 0, + &result) || result != WIRELOG_TYPE_UINT64){ + FAIL("opaque prefix rescued by known suffix"); return; + } + } + /* Nullary UUID4 must respect the stack bound as a push. */ + memset(expression, WL_PLAN_EXPR_ARITH_UUID4, COL_FILTER_STACK+1); + uint32_t size = COL_FILTER_STACK+1; + for (unsigned i = 1; i < COL_FILTER_STACK+1; + i++) expression[size++] = WL_PLAN_EXPR_ARITH_ADD; + if (wl_columnar_expr_interpreted_result_type(expression, size, NULL, 0, + &result)){ + FAIL("nullary stack overflow"); return; + } + const uint8_t values[] = {WL_PLAN_EXPR_CONST_INT, WL_PLAN_EXPR_CONST_FLOAT, + WL_PLAN_EXPR_BOOL}; + const wirelog_column_type_t expected[] = {WIRELOG_TYPE_INT64, + WIRELOG_TYPE_FLOAT, + WIRELOG_TYPE_BOOL}; + for (unsigned i = 0; i < 3; i++) { + expression[0] = values[i]; memset(expression+1, 0, 8); + size = i == 2 ? 2 : 9; + if (!wl_columnar_expr_interpreted_result_type(expression, size, NULL, 0, + &result) || result != expected[i]) { + FAIL("constant type"); return; + } + for (uint32_t n = 0; n < size; + n++) if (wl_columnar_expr_interpreted_result_type(expression, n, + NULL, 0, &result)) { + FAIL("truncated constant"); return; + } + } + PASS(); +} + +static void test_empty_string_map_schema(void) +{ + TEST( + "empty MAP keeps direct STRING beside interpreted CAT; unknown stays untyped"); + col_rel_t *input = col_rel_new_auto("input", 1); + col_rel_t *output = col_rel_new_auto("output", 2); + wl_col_session_t session = {0}; + wirelog_column_type_t types[] = {WIRELOG_TYPE_STRING}; + uint8_t direct[32], cat[64]; + uint32_t a = string_operand(direct, WL_PLAN_EXPR_VAR, "col0"); + memcpy(cat, direct, a); uint32_t b = a; + b += string_operand(cat+b, WL_PLAN_EXPR_CONST_STR, "suffix"); + cat[b++] = WL_PLAN_EXPR_STR_FN_CAT; + wl_plan_expr_buffer_t expressions[] = {{.data = direct, .size = a}, + {.data = cat, .size = b}}; + uint32_t indices[] = {0, 0}; + wl_plan_op_t map = {.op = WL_PLAN_OP_MAP, .project_count = 2, + .project_indices = indices, .map_expr_count = 2, + .map_exprs = expressions}; + bool ok = input && output && col_rel_set_column_types(input, types, + 1) == 0 && + col_op_map_output_types(&map, input, &session, NULL, output) == 0 && + output->column_types && + output->column_types[0] == WIRELOG_TYPE_STRING && + output->column_types[1] == WIRELOG_TYPE_STRING && !output->nrows; + col_rel_destroy(output); output = col_rel_new_auto("unknown", 2); + cat[b-1] = WL_PLAN_EXPR_EXTENSION_CALL; + ok = ok && output && col_op_map_output_types(&map, input, &session, NULL, + output) == 0 && !output->column_types; + col_rel_destroy(output); col_rel_destroy(input); + if (!ok) { + FAIL("empty type publication or unknown fallback"); return; + } + PASS(); +} + int main(void) { printf("=== wirelog Pre-compiled Expression Evaluator Tests (Issue #298)" " ===\n\n"); + test_string_result_classification(); + test_empty_string_map_schema(); + test_builtin_result_contracts(); printf("--- FILTER: arithmetic comparison ---\n"); test_filter_crdt_arithmetic(); test_filter_arithmetic_vs_constant(); diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 4635564ec..5cef9cebf 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -1522,7 +1522,7 @@ static void multiple_schema_audit(void) for (unsigned phase = 0; phase < 2; phase++) { b->column_types = types; if (bad == 0) types[2] = WIRELOG_TYPE_FLOAT; - if (bad == 1) types[1] = WIRELOG_TYPE_STRING; /* Nonprojected lane. */ + if (bad == 1) types[1] = (wirelog_column_type_t)99; /* Unsupported nonprojected lane. */ if (bad == 2) b->has_graph_column = true; if (bad == 3) b->compound_count = 1; if (bad == 4) b->ncols = 2; /* Predicate col2 must use B's width. */ @@ -2084,6 +2084,221 @@ static void cold_intermediate_floor(void) } } +static const char *reference_projection_query = + ".decl a(k:int64,s:symbol,u:symbol)\n.decl b(s:symbol,k:int64)\n" + ".decl result(s:symbol,k:int64,t:symbol)\n.decl other(s:symbol,k:int64)\n" + "result(s,k,s) :- a(k,s,u), k >= 3.\nother(s,k) :- b(s,k).\n"; +static const char *reference_values[] = {"", "alpha", "한글"}; +static fixture_t reference_projection_fixture(uint64_t allowance, unsigned rows) +{ + fixture_t f = fixture(reference_projection_query, allowance, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + int64_t ids[3]; + for (unsigned i = 0; i < 3; i++) { + ids[i] = wl_intern_put(s->intern, reference_values[i]); + CHECK(ids[i] >= 0); + } + for (unsigned i = 0; i < (rows ? rows : 1); i++) { + int64_t a[] = {(int64_t)i, ids[i%3], ids[(i+1)%3]}, + b[] = {ids[i%3], (int64_t)i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + if (!rows) { + CHECK(wl_session_remove(f.session, "a", a, 1, 3) == 0); + CHECK(wl_session_remove(f.session, "b", b, 1, 2) == 0); + } + } + return f; +} +typedef struct { wl_intern_t *intern; unsigned bits[2]; } reference_events_t; +static void reference_observe(const char *name, const int64_t *row, + uint32_t cols, void *context) +{ + reference_events_t *e = context; + unsigned idx = !strcmp(name, "result") ? 0u : 1u; + CHECK(idx == 0 || !strcmp(name, "other")); + CHECK(cols == (idx ? 2u : 3u) && row[1] >= (idx ? 0 : 3) && row[1] < 9); + CHECK(!strcmp(wl_intern_reverse(e->intern, row[0]), + reference_values[(unsigned)row[1]%3])); + if (!idx) CHECK(row[2] == row[0]); + unsigned bit = 1u << (unsigned)row[1]; CHECK(!(e->bits[idx]&bit)); + e->bits[idx] |= bit; +} +static void reference_projections(void) +{ + for (unsigned empty = 0; empty < 2; empty++) + for (unsigned order = 0; order < 2; order++) + for (unsigned count = 0; count <= 2; count++) { + fixture_t f = reference_projection_fixture(4*1024*1024, + empty ? 0 : 9); + wl_col_session_t *s = COL_SESSION(f.session); + const char *names[] = {"a", "b"}; + for (unsigned i = 0; i < count; + i++) CHECK(wl_columnar_session_seal_input(f.session, + names[order ? 1-i : i], NULL, NULL) == OK); + if (order) CHECK(wl_session_step(f.session) == 0); + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_events_t e = {.intern = s->intern}; + CHECK(wl_session_snapshot(f.session, reference_observe, + &e) == 0); + CHECK(e.bits[0] == (empty ? 0u : 504u) && + e.bits[1] == (empty ? 0u : 511u)); + } + col_rel_t *r = session_find_rel(s, "result"); + if (!empty) CHECK(r && r->column_types && + r->column_types[0] == WIRELOG_TYPE_STRING && + r->column_types[2] == WIRELOG_TYPE_STRING); + destroy(&f); + } +} +static void reference_projection_failures(void) +{ + for (unsigned fault = 0; fault < 6; fault++) { + fixture_t f = reference_projection_fixture(4*1024*1024, 9); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, NULL) == OK); + col_rel_t *source = session_find_rel(s, "a"); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + int64_t saved = source->columns[2][8]; + wl_intern_t *intern = s->intern, *foreign = NULL; + if (fault == 0) source->columns[2][8] = INT64_MAX; /* Unprojected invalid ID. */ + if (fault == 1) s->intern = NULL; + if (fault == 2) { + foreign = wl_intern_create(); CHECK(foreign); s->intern = foreign; + } + if (fault == 3) allocation_fault = 1; + if (fault == 4) write_fault = 1; + cancellation_t cancellation = {.stop = 3}; + wl_columnar_spill_status_t rc = + wl_columnar_session_seal_input(f.session, "a", + fault == 5 ? cancel : NULL, &cancellation); + CHECK(rc != OK && session_find_rel(s, + "a") == source && !wl_columnar_session_cold_find(s, "a")); + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + s->intern = intern; if (foreign) wl_intern_free(foreign); + source->columns[2][8] = saved; allocation_fault = write_fault = 0; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + const wl_plan_relation_t *rp = join_body(&f, "result"); + eval_stack_t stack; eval_stack_init(&stack); before = reserved(&f); + wl_columnar_ops_test_map_fail_output_alloc = true; + CHECK(wl_columnar_ops_cold_projection(rp, &stack, s) == ENOMEM); + wl_columnar_ops_test_map_fail_output_alloc = false; + CHECK(!stack.top && reserved(&f) == before); + s->intern = NULL; + CHECK(wl_columnar_ops_cold_projection(rp, &stack, + s) == ENOTSUP && !stack.top); + s->intern = intern; + read_fault = 9; unsigned events = 0; + CHECK(wl_session_snapshot(f.session, count_only, + &events) == EIO && events == 0); + read_fault = 0; reference_events_t e = {.intern = intern}; + CHECK(wl_session_snapshot(f.session, reference_observe, + &e) == 0 && e.bits[0] == 504 && e.bits[1] == 511); + destroy(&f); + } +} +static void reference_projection_bytecode(void) +{ + fixture_t f = reference_projection_fixture(4*1024*1024, 9); + wl_col_session_t *s = COL_SESSION(f.session); + wl_plan_relation_t *rp = join_body(&f, "result"); + wl_plan_op_t *map = (wl_plan_op_t *)&rp->ops[rp->op_count-2]; + CHECK(map->map_expr_count && map->map_exprs[0].data[0] == WL_PLAN_EXPR_VAR); + uint8_t *data = (uint8_t *)map->map_exprs[0].data; + data[0] = WL_PLAN_EXPR_VAR_FLOAT; + uint64_t before = reserved(&f); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + CHECK(reserved(&f) == before && session_find_rel(s, "a")); + data[0] = WL_PLAN_EXPR_VAR; + uint8_t *numeric = (uint8_t *)map->map_exprs[1].data; + numeric[0] = WL_PLAN_EXPR_VAR_STRING; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + CHECK(reserved(&f) == before && session_find_rel(s, "a")); + numeric[0] = WL_PLAN_EXPR_VAR; + data[0] = WL_PLAN_EXPR_VAR_STRING; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + destroy(&f); +} + +static void reference_wide_projection(fixture_t *f, wl_plan_relation_t *wide, + wl_plan_op_t ops[4], uint32_t indices[32]) +{ + *wide = *join_body(f, "result"); CHECK(wide->op_count == 4); + memcpy(ops, wide->ops, 4*sizeof(*ops)); wide->ops = ops; + for (unsigned i = 0; i < 32; i++) indices[i] = 1; + ops[2].project_indices = indices; ops[2].project_count = 32; + ops[2].map_expr_count = 0; ops[2].map_exprs = NULL; +} +static void reference_projection_memory(void) +{ + wl_columnar_memory_governor_test_admitted = budget_admitted; + budget_peak = 0; + fixture_t calibration = reference_projection_fixture(4*1024*1024, 1024); + CHECK(wl_columnar_session_seal_input(calibration.session, "a", NULL, + NULL) == OK); + wl_plan_relation_t wide; wl_plan_op_t ops[4]; uint32_t indices[32]; + reference_wide_projection(&calibration, &wide, ops, indices); + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_ops_cold_projection(&wide, &stack, + COL_SESSION(calibration.session)) == 0); + uint64_t allowance = budget_peak; + eval_entry_t entry = eval_stack_pop(&stack); + CHECK(entry.rel->nrows == 1021 && + entry.rel->column_types[0] == WIRELOG_TYPE_STRING); + CHECK(eval_stack_dispose_entry(&stack, &entry) == 0); destroy(&calibration); + for (unsigned retain = 0; retain < 2; retain++) { + budget_peak = 0; + fixture_t f = reference_projection_fixture(allowance, 1024); + wl_col_session_t *s = COL_SESSION(f.session); + wl_columnar_session_test_retain_cold_source = retain != 0; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + wl_columnar_session_test_retain_cold_source = false; + reference_wide_projection(&f, &wide, ops, indices); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + int rc = wl_columnar_ops_cold_projection(&wide, &stack, s); + CHECK(budget_peak <= allowance); + if (retain) CHECK(rc == ENOSPC && !stack.top && + wl_session_budget_denied(f.session)); + else { + CHECK(rc == 0); entry = eval_stack_pop(&stack); + CHECK(entry.rel->nrows == 1021 && + entry.rel->column_types[0] == WIRELOG_TYPE_STRING); + CHECK(eval_stack_dispose_entry(&stack, &entry) == 0); + } + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + } + wl_columnar_memory_governor_test_admitted = NULL; + /* Intern storage remains resident. Real distinct strings eventually exhaust + * a fixed allowance even though the row source has already been sealed. */ + fixture_t f = reference_projection_fixture(256*1024, 9); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + bool denied = false; + for (unsigned i = 0; i < 16384; i++) { + char value[256]; memset(value, 'x', sizeof(value)); + int length = snprintf(value, 32, "distinct-%u-", i); CHECK(length > 0); + value[length] = 'x'; value[sizeof(value)-1] = '\0'; + int64_t id = -1; + int rc = wl_intern_put_checked(s->intern, value, &id); + if (rc == WL_INTERN_ERR_MEMORY_BUDGET) { + denied = true; break; + } + CHECK(rc == 0 && id >= 0); + } + CHECK(denied && reserved(&f) > before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -2093,6 +2308,9 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + reference_projection_memory(); + reference_projections(); reference_projection_failures(); + reference_projection_bytecode(); cold_intermediate_floor(); directional_constructor_floor(); directional_reclamation(); directional_bounded_scratch(); diff --git a/tests/test_string_eval.c b/tests/test_string_eval.c index 7b85f7086..849a07f93 100644 --- a/tests/test_string_eval.c +++ b/tests/test_string_eval.c @@ -944,12 +944,47 @@ test_eval_strlen_multiple_inputs(void) /* Main */ /* ======================================================================== */ +static void test_mixed_builtin_result_schema(void) +{ + TEST( + "mixed builtin final producers preserve numeric, BOOL and STRING types"); + wirelog_error_t error; + wirelog_program_t *program = wirelog_parse_string( + ".decl a(s:symbol)\n.decl r(n:int64,len:int64,yes:int64,text:symbol)\n" + "a(\"41\").\nr(to_number(s)+1,strlen(to_upper(s)),contains(s,\"4\"),to_string(strlen(s))):-a(s).\n", + &error); + wl_plan_t *plan = NULL; wl_session_t *session = NULL; + bool ok = program && wl_plan_from_program(program, &plan) == 0 && + wl_session_create(wl_backend_columnar(), plan, 1, &session) == 0 && + wl_session_load_facts(session, program) == 0; + struct result_ctx rows = {0}; + if (ok) ok = wl_session_snapshot(session, capture_cb, &rows) == 0; + col_rel_t *r = ok ? session_find_rel(COL_SESSION(session), "r") : NULL; + ok = ok && r && r->ncols == 4 && r->nrows == 1 && r->column_types && + r->column_types[0] == WIRELOG_TYPE_INT64 && + r->column_types[1] == WIRELOG_TYPE_INT64 && + r->column_types[2] == WIRELOG_TYPE_BOOL && + r->column_types[3] == WIRELOG_TYPE_STRING && + r->columns[0][0] == 42 && r->columns[1][0] == 2 && + r->columns[2][0] == 1; + const char *value = ok ? wl_intern_reverse(COL_SESSION(session)->intern, + r->columns[3][0]) : NULL; + ok = ok && value && !strcmp(value, "2"); + if (session) wl_session_destroy(session); + wl_plan_free(plan); if (program) wirelog_program_free(program); + if (!ok) { + FAIL("mixed result values or schema lost"); return; + } + PASS(); +} + int main(void) { printf( "=== String Function Evaluator Integration Tests (Issue #143) ===\n"); + test_mixed_builtin_result_schema(); printf("\n--- strlen via to_string ---\n"); test_eval_strlen_via_to_string_2digits(); test_eval_strlen_via_to_string_3digits(); diff --git a/tests/test_wirelog_advanced.c b/tests/test_wirelog_advanced.c index cbb1a7a25..ec35e3ea1 100644 --- a/tests/test_wirelog_advanced.c +++ b/tests/test_wirelog_advanced.c @@ -2581,7 +2581,8 @@ reserved_on(wl_columnar_memory_governor_ref_t *ref) static int measure_create_floor(wirelog_program_t *prog, uint64_t *intern_bytes, uint64_t *compound_bytes, - uint64_t *descriptor_bytes, uint64_t *diff_registry_bytes); + uint64_t *descriptor_bytes, uint64_t *diff_registry_bytes, + uint64_t *schema_scratch); static int test_inline_fact_budget_denial_maps(void) @@ -2599,7 +2600,7 @@ test_inline_fact_budget_denial_maps(void) int rc = 1; if (!prog || measure_create_floor(prog, &intern_bytes, &compound_bytes, - &descriptor_bytes, &diff_registry_bytes) != 0) + &descriptor_bytes, &diff_registry_bytes, NULL) != 0) goto out; if (!wl_columnar_memory_size_add(intern_bytes, compound_bytes, &floor) || !wl_columnar_memory_size_add(floor, SESSION_REGISTRY_BYTES, @@ -2685,7 +2686,7 @@ test_count_controlled_diff_join_candidates(const wl_plan_op_t *ops, static int measure_create_floor(wirelog_program_t *prog, uint64_t *intern_bytes, uint64_t *compound_bytes, uint64_t *descriptor_bytes, - uint64_t *diff_registry_bytes) + uint64_t *diff_registry_bytes, uint64_t *schema_scratch) { wl_columnar_memory_governor_ref_t *probe = enforcing_governor(UINT64_MAX / 4u); @@ -2702,6 +2703,7 @@ measure_create_floor(wirelog_program_t *prog, uint64_t *intern_bytes, goto out; *descriptor_bytes = 0; *diff_registry_bytes = 0; + if (schema_scratch) *schema_scratch = 0; for (uint32_t i = 0; i < warm->edb_count; i++) { size_t name_len = strlen(warm->edb_relations[i]); uint64_t name_bytes, relation_bytes; @@ -2713,6 +2715,29 @@ measure_create_floor(wirelog_program_t *prog, uint64_t *intern_bytes, || !wl_columnar_memory_size_add(*descriptor_bytes, relation_bytes, descriptor_bytes)) goto out; + bool typed = false; + uint32_t width = warm->edb_column_type_counts[i]; + for (uint32_t c = 0; c < width; c++) + typed |= warm->edb_column_types[i][c] == WIRELOG_TYPE_STRING || + warm->edb_column_types[i][c] == WIRELOG_TYPE_FLOAT; + if (typed) { + col_rel_t *relation = NULL; + if (wl_columnar_relation_alloc_governed(&relation, + warm->edb_relations[i], probe) != 0) goto out; + uint64_t descriptor = reserved_on(probe); + int status = col_rel_set_schema(relation, width, NULL); + uint64_t transient = reserved_on(probe) - descriptor; + if (!status) status = col_rel_set_column_types(relation, + warm->edb_column_types[i], width); + uint64_t metadata = reserved_on(probe) - descriptor; + col_rel_destroy(relation); + if (status || reserved_on(probe) != 0 || + !wl_columnar_memory_size_add(*descriptor_bytes, metadata, + descriptor_bytes)) goto out; + /* Type publication holds the old empty schema until replacement. */ + if (schema_scratch && transient > *schema_scratch) + *schema_scratch = transient; + } } uint64_t candidates = 0; for (uint32_t s = 0; s < warm->stratum_count; s++) { @@ -2784,7 +2809,7 @@ test_bulk_insert_error_mapping(void) int rc = 1; if (!prog || measure_create_floor(prog, &intern_bytes, - &compound_bytes, &descriptor_bytes, &diff_registry_bytes) != 0) { + &compound_bytes, &descriptor_bytes, &diff_registry_bytes, NULL) != 0) { fprintf(stderr, "T-1938: could not measure session floor\n"); goto out; } @@ -2948,7 +2973,7 @@ test_denied_eval_interning_maps_to_memory(void) if (!prog) return 1; if (measure_create_floor(prog, &intern_bytes, &compound_bytes, - &descriptor_bytes, &diff_registry_bytes) != 0) { + &descriptor_bytes, &diff_registry_bytes, NULL) != 0) { fprintf(stderr, "T-1521: could not measure the create floor\n"); wirelog_program_free(prog); return 1; @@ -3019,12 +3044,13 @@ test_injected_governor_denial_maps_to_memory(void) uint64_t compound_bytes = 0; uint64_t descriptor_bytes = 0; uint64_t diff_registry_bytes = 0; + uint64_t schema_scratch = 0; int rc = 1; if (!prog) return 1; if (measure_create_floor(prog, &intern_bytes, &compound_bytes, - &descriptor_bytes, &diff_registry_bytes) != 0) { + &descriptor_bytes, &diff_registry_bytes, &schema_scratch) != 0) { fprintf(stderr, "T-1473: could not measure the create-time floor\n"); goto out; } @@ -3058,11 +3084,11 @@ test_injected_governor_denial_maps_to_memory(void) } wl_columnar_memory_governor_ref_release(ref); - /* Exact floor: creation succeeds, destroy leaves the program-owned - * intern reservation, and freeing the program releases it. */ + /* Retained floor plus measured schema replacement scratch: creation + * succeeds, destroy leaves only the program-owned intern reservation. */ ref = enforcing_governor(intern_bytes + compound_bytes + SESSION_REGISTRY_BYTES + diff_registry_bytes - + descriptor_bytes); + + descriptor_bytes + schema_scratch); if (!ref) goto out; options.memory_governor = ref; @@ -3070,11 +3096,16 @@ test_injected_governor_denial_maps_to_memory(void) error = wirelog_session_create(prog, WIRELOG_BACKEND_COLUMNAR, 1, &session); wl_session_testhook_set_default_options(NULL); + uint64_t retained_floor = intern_bytes + compound_bytes + + SESSION_REGISTRY_BYTES + diff_registry_bytes + descriptor_bytes; + /* Replacement slack may also admit the optional registry hash. The + * mandatory retained schema must be present, and no reservation may + * exceed the measured creation allowance. Destruction below proves all + * schema/hash scratch is refunded. */ if (error != WIRELOG_OK || !session - || reserved_on(ref) != intern_bytes + compound_bytes - + SESSION_REGISTRY_BYTES + diff_registry_bytes - + descriptor_bytes) { - fprintf(stderr, "T-1473: exact-fit create err=%d reserved=%llu\n", + || reserved_on(ref) < retained_floor + || reserved_on(ref) > retained_floor + schema_scratch) { + fprintf(stderr, "T-1473: measured-floor create err=%d reserved=%llu\n", error, (unsigned long long)reserved_on(ref)); goto out; } @@ -3323,6 +3354,63 @@ test_options_null_defaults(void) return failed; } +typedef struct { + const wl_intern_t *intern; + const char *expected; + unsigned count; + bool invalid; +} builtin_rows_t; +static void builtin_row(const char *name, const int64_t *row, uint32_t cols, + void *context) +{ + builtin_rows_t *out = context; + if (strcmp(name, "out"))return; + out->count++; + const char *k = cols > 0 ? wl_intern_reverse(out->intern, row[0]) : NULL; + const char *v = cols > 1 ? wl_intern_reverse(out->intern, row[1]) : NULL; + const char *x = cols > 2 ? wl_intern_reverse(out->intern, row[2]) : NULL; + if (cols != 3 || !k || strcmp(k, "k") || !v || strcmp(v, + out->expected) || !x || strcmp(x, "x")) out->invalid = true; +} +static int test_builtin_string_pipeline_types(void) +{ + const struct { const char *expression, *expected; } cases[] = { + {"to_upper(s)", "ALPHA"}, {"to_lower(s)", "alpha"}, + {"cat(s,\"!\")", "alpha!"}, + {"substr(to_upper(s),0,strlen(s))", "ALPHA"}, + {"str_replace(s,\"ph\",to_upper(\"ph\"))", "alPHa"}, + {"trim(cat(\" \",cat(s,\" \")))", "alpha"}, + {"to_string(strlen(s)+1)", "6"}, + {"to_string(to_number(\"40\")+2)", "42"}, + {"to_string(contains(s,\"ph\"))", "1"} + }; + for (unsigned i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { + char source[1024]; + snprintf(source, sizeof(source), + ".decl a(k:symbol,s:symbol)\n.decl b(k:symbol,t:symbol)\n" + ".decl d(k:symbol,x:symbol)\n.decl out(k:symbol,t:symbol,x:symbol)\n" + "a(\"k\",\"alpha\").\nd(\"k\",\"x\").\nb(k,%s):-a(k,s).\n" + "out(k,t,x):-a(k,s),b(k,t),d(k,x).\n", cases[i].expression); + wirelog_program_t *p = parse_or_die(source, "STRING-builtin-pipeline"); + if (!p) return 1; + wirelog_session_t *session = NULL; + wirelog_error_t error = wirelog_session_create(p, + WIRELOG_BACKEND_DEFAULT, 1, &session); + builtin_rows_t rows = + {.intern = (const wl_intern_t *)wirelog_program_get_intern(p), + .expected = cases[i].expected}; + if (!error) error = wirelog_session_snapshot(session, builtin_row, + &rows); + wirelog_session_destroy(session); wirelog_program_free(p); + if (error || rows.invalid || rows.count != 1) { + fprintf(stderr, "STRING builtin %s: error=%d rows=%u invalid=%d\n", + cases[i].expression, error, rows.count, rows.invalid); + return 1; + } + } + return 0; +} + int main(void) { @@ -3344,6 +3432,7 @@ main(void) failures += test_insert_remove_roundtrip(); failures += test_null_safety(); failures += test_open_parse_error(); + failures += test_builtin_string_pipeline_types(); failures += test_num_workers_default_is_one(); failures += test_num_workers_explicit_four(); failures += test_inline_compound_body_binding(); diff --git a/tests/test_wirelog_advanced_spill.c b/tests/test_wirelog_advanced_spill.c index 2d423a2a9..cd79da03f 100644 --- a/tests/test_wirelog_advanced_spill.c +++ b/tests/test_wirelog_advanced_spill.c @@ -763,6 +763,80 @@ static void join_chains(void) } } +typedef struct { int64_t ids[3]; unsigned bits[2]; } reference_public_events_t; +static void reference_public_observe(const char *name, + const wirelog_typed_row_v1_t *row, int32_t diff, void *context) +{ + reference_public_events_t *e = context; + unsigned idx = !strcmp(name, "result") ? 0u : 1u; + CHECK(idx == 0 || !strcmp(name, "other")); + unsigned width = idx ? 2 : 3; + CHECK(diff == 1 && row && row->logical_ncols == width && + row->physical_nlanes == width); + CHECK(row->types[0] == WIRELOG_TYPE_STRING && + row->physical_types[0] == WIRELOG_TYPE_STRING && + row->types[1] == WIRELOG_TYPE_INT64); + unsigned key = (unsigned)row->lanes[1]; + CHECK(key < 3 && row->lanes[0] == (uint64_t)e->ids[key]); + if (!idx) CHECK(row->types[2] == WIRELOG_TYPE_STRING && + row->physical_types[2] == WIRELOG_TYPE_STRING && + row->lanes[2] == row->lanes[0]); + CHECK(!(e->bits[idx]&(1u<bits[idx] |= 1u<= 1" : ""); + wirelog_program_t *p = parse(text); wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, + WIRELOG_BACKEND_DEFAULT, 1, &o, &s) == WIRELOG_OK); + int64_t *facts = NULL; uint32_t rows = 0, cols = 0; + CHECK(wirelog_program_get_facts(p, "a", &facts, &rows, + &cols) == 0 && rows == 3 && cols == 3); + reference_public_events_t expected = {0}; + for (unsigned i = 0; i < rows; i++) { + CHECK(facts[i*cols] >= 0 && facts[i*cols] < 3); + expected.ids[facts[i*cols]] = facts[i*cols+1]; + if (empty) CHECK(wirelog_session_remove(s, "a", + facts+i*cols, 1, cols) == WIRELOG_OK); + } + if (!empty) CHECK(wirelog_session_insert(s, "a", facts, 1, + cols) == WIRELOG_OK); /* Duplicate row. */ + free(facts); facts = NULL; + CHECK(wirelog_program_get_facts(p, "b", &facts, &rows, + &cols) == 0 && rows == 3 && cols == 2); + if (empty) CHECK(wirelog_session_remove(s, "b", facts, rows, + cols) == WIRELOG_OK); + free(facts); + if (order) { + CHECK(wirelog_session_seal_input(s, + order == 1 ? "a" : "b") == WIRELOG_OK); + CHECK(wirelog_session_seal_join_input(s, + order == 1 ? "b" : "a") == WIRELOG_OK); + } + if (order == 2) CHECK(wirelog_session_step(s) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_public_events_t e = expected; + CHECK(wirelog_session_snapshot_typed(s, + reference_public_observe, &e) == WIRELOG_OK); + CHECK(e.bits[0] == (empty ? 0u : filtered ? 6u : 7u) && + e.bits[1] == (empty ? 0u : 7u)); + } + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + int main(void) { /* Issue #2287: a session created with a nonzero WIRELOG_JOIN_BATCH_BYTES @@ -781,6 +855,7 @@ int main(void) test_tmpdir()); CHECK(mkdtemp(directory)); #endif + reference_public_projections(); join_directions(); join_chains(); descriptors(); lifecycle(); refusals(); typed_projection(); inline_facts(); sealed_sets(); standalone_reads(); output_floor(); mixed_projection(); diff --git a/wirelog/columnar/batch_payload.c b/wirelog/columnar/batch_payload.c index 5887f0b5c..49346f7ea 100644 --- a/wirelog/columnar/batch_payload.c +++ b/wirelog/columnar/batch_payload.c @@ -574,6 +574,15 @@ wl_columnar_batch_payload_read(wl_columnar_batch_payload_reader_t *r, return WL_COLUMNAR_SPILL_OK; } +bool +wl_columnar_batch_payload_uses_intern(const wl_columnar_batch_payload_t *p, + const wl_intern_t *intern) +{ + return p && intern && (p->segment ? + wl_columnar_spill_segment_uses_intern(p->segment, intern) : + p->intern == intern); +} + const wirelog_column_type_t * wl_columnar_batch_payload_schema(const wl_columnar_batch_payload_t *p, uint32_t *ncols, uint32_t *nrows) diff --git a/wirelog/columnar/batch_payload.h b/wirelog/columnar/batch_payload.h index 2b5ca9178..d6fe15dab 100644 --- a/wirelog/columnar/batch_payload.h +++ b/wirelog/columnar/batch_payload.h @@ -161,6 +161,12 @@ const wirelog_column_type_t *wl_columnar_batch_payload_schema( const wl_columnar_batch_payload_t *payload, uint32_t *ncols, uint32_t *nrows); +/* Identity of the retained provenance, including spilled segment ownership. + * Caller holds a live payload owner/reader. This neither acquires an intern + * pin nor tests whether its original owner is open. Numeric payloads return false. */ +bool wl_columnar_batch_payload_uses_intern( + const wl_columnar_batch_payload_t *payload, const wl_intern_t *intern); + /* Exact admission owner identity; no ownership transfer. */ bool wl_columnar_batch_payload_uses_governor( const wl_columnar_batch_payload_t *payload, diff --git a/wirelog/columnar/eval_plan.c b/wirelog/columnar/eval_plan.c index 356ba84c3..e68619d3c 100644 --- a/wirelog/columnar/eval_plan.c +++ b/wirelog/columnar/eval_plan.c @@ -433,7 +433,8 @@ wl_columnar_eval_plan_cold_projection_decode(const wl_plan_relation_t *rp, if (!e->size && !e->data) continue; if (!e->data || e->size < 7 || (e->data[0] != WL_PLAN_EXPR_VAR && - e->data[0] != WL_PLAN_EXPR_VAR_FLOAT)) return false; + e->data[0] != WL_PLAN_EXPR_VAR_FLOAT && + e->data[0] != WL_PLAN_EXPR_VAR_STRING)) return false; uint16_t len; memcpy(&len, e->data + 1, sizeof(len)); if ((uint32_t)len + 3 != e->size || @@ -446,7 +447,9 @@ wl_columnar_eval_plan_cold_projection_decode(const wl_plan_relation_t *rp, } if (col != m->project_indices[i] || ((e->data[0] == WL_PLAN_EXPR_VAR_FLOAT) != - (types && types[col] == WIRELOG_TYPE_FLOAT))) return false; + (types && types[col] == WIRELOG_TYPE_FLOAT)) || + (e->data[0] == WL_PLAN_EXPR_VAR_STRING && + (!types || types[col] != WIRELOG_TYPE_STRING))) return false; } if (out) *out = decoded; return true; diff --git a/wirelog/columnar/expr.c b/wirelog/columnar/expr.c index caceb1ab8..d32425e26 100644 --- a/wirelog/columnar/expr.c +++ b/wirelog/columnar/expr.c @@ -3081,6 +3081,154 @@ wl_columnar_expr_compile(const uint8_t *buf, uint32_t size, NULL); } +/* Static result contracts only: bounded depth and a complete single-result + * buffer identify the final producer without evaluation or operand storage. */ +bool +wl_columnar_expr_interpreted_result_type(const uint8_t *buf, uint32_t size, + const wirelog_column_type_t *types, uint32_t ncols, + wirelog_column_type_t *type) +{ + if (!buf || !size || !type) return false; + uint32_t pos = 0, depth = 0; + wirelog_column_type_t result = WIRELOG_TYPE_INT64; + while (pos < size) { + uint8_t tag = buf[pos++]; + uint32_t arity = 0; + switch ((wl_plan_expr_tag_t)tag) { + case WL_PLAN_EXPR_VAR: + case WL_PLAN_EXPR_VAR_FLOAT: + case WL_PLAN_EXPR_VAR_STRING: + case WL_PLAN_EXPR_CONST_STR: { + if (size - pos < sizeof(uint16_t)) return false; + uint16_t length; + memcpy(&length, buf + pos, sizeof(length)); + pos += sizeof(length); + if (length > size - pos) return false; + result = WIRELOG_TYPE_STRING; + if (tag != WL_PLAN_EXPR_CONST_STR) { + if (length < 4 || length > 12 || memcmp(buf + pos, "col", 3)) + return false; + uint32_t col = 0; + for (uint32_t j = 3; j < length; j++) { + unsigned digit = (unsigned)buf[pos + j] - '0'; + if (digit > 9 || col > (UINT32_MAX - digit) / 10) + return false; + col = col * 10 + digit; + } + if (col >= ncols) return false; + result = types ? types[col] : WIRELOG_TYPE_INT64; + if (result < WIRELOG_TYPE_INT32 || result > WIRELOG_TYPE_BOOL || + (tag == WL_PLAN_EXPR_VAR_STRING && + result != WIRELOG_TYPE_STRING) || + (tag == WL_PLAN_EXPR_VAR_FLOAT && + result != WIRELOG_TYPE_FLOAT)) + return false; + } + pos += length; + break; + } + case WL_PLAN_EXPR_CONST_INT: + case WL_PLAN_EXPR_CONST_FLOAT: + if (size - pos < 8) return false; + pos += 8; + result = tag == + WL_PLAN_EXPR_CONST_FLOAT ? WIRELOG_TYPE_FLOAT : + WIRELOG_TYPE_INT64; + break; + case WL_PLAN_EXPR_BOOL: + if (pos == size) return false; + ++pos; result = WIRELOG_TYPE_BOOL; + break; + case WL_PLAN_EXPR_ARITH_ADD: + case WL_PLAN_EXPR_ARITH_SUB: + case WL_PLAN_EXPR_ARITH_MUL: + case WL_PLAN_EXPR_ARITH_DIV: + case WL_PLAN_EXPR_ARITH_MOD: + case WL_PLAN_EXPR_ARITH_BAND: + case WL_PLAN_EXPR_ARITH_BOR: + case WL_PLAN_EXPR_ARITH_BXOR: + case WL_PLAN_EXPR_ARITH_SHL: + case WL_PLAN_EXPR_ARITH_SHR: + case WL_PLAN_EXPR_ARITH_HMAC_SHA256: + case WL_PLAN_EXPR_ARITH_UUID5: + case WL_PLAN_EXPR_ARITH_HMAC_SHA256_SS: + case WL_PLAN_EXPR_ARITH_HMAC_SHA256_SI: + case WL_PLAN_EXPR_ARITH_HMAC_SHA256_IS: + case WL_PLAN_EXPR_ARITH_UUID5_SS: + case WL_PLAN_EXPR_ARITH_UUID5_SI: + case WL_PLAN_EXPR_ARITH_UUID5_IS: + arity = 2; result = WIRELOG_TYPE_INT64; break; + case WL_PLAN_EXPR_ARITH_BNOT: + case WL_PLAN_EXPR_ARITH_HASH: + case WL_PLAN_EXPR_ARITH_CRC32_ETH: + case WL_PLAN_EXPR_ARITH_CRC32_CAST: + case WL_PLAN_EXPR_ARITH_MD5: + case WL_PLAN_EXPR_ARITH_SHA1: + case WL_PLAN_EXPR_ARITH_SHA256: + case WL_PLAN_EXPR_ARITH_SHA512: + case WL_PLAN_EXPR_ARITH_HASH_S: + case WL_PLAN_EXPR_ARITH_CRC32_ETH_S: + case WL_PLAN_EXPR_ARITH_CRC32_CAST_S: + case WL_PLAN_EXPR_ARITH_MD5_S: + case WL_PLAN_EXPR_ARITH_SHA1_S: + case WL_PLAN_EXPR_ARITH_SHA256_S: + case WL_PLAN_EXPR_ARITH_SHA512_S: + case WL_PLAN_EXPR_STR_FN_STRLEN: + case WL_PLAN_EXPR_STR_FN_STR_ORD: + case WL_PLAN_EXPR_STR_FN_TO_NUMBER: + arity = 1; result = WIRELOG_TYPE_INT64; break; + case WL_PLAN_EXPR_ARITH_UUID4: + arity = 0; result = WIRELOG_TYPE_INT64; break; + case WL_PLAN_EXPR_ARITH_FLOAT_ADD: + case WL_PLAN_EXPR_ARITH_FLOAT_SUB: + case WL_PLAN_EXPR_ARITH_FLOAT_MUL: + case WL_PLAN_EXPR_ARITH_FLOAT_DIV: + arity = 2; result = WIRELOG_TYPE_FLOAT; break; + case WL_PLAN_EXPR_CMP_EQ: + case WL_PLAN_EXPR_CMP_NEQ: + case WL_PLAN_EXPR_CMP_LT: + case WL_PLAN_EXPR_CMP_GT: + case WL_PLAN_EXPR_CMP_LTE: + case WL_PLAN_EXPR_CMP_GTE: + case WL_PLAN_EXPR_CMP_FLOAT_EQ: + case WL_PLAN_EXPR_CMP_FLOAT_NEQ: + case WL_PLAN_EXPR_CMP_FLOAT_LT: + case WL_PLAN_EXPR_CMP_FLOAT_GT: + case WL_PLAN_EXPR_CMP_FLOAT_LTE: + case WL_PLAN_EXPR_CMP_FLOAT_GTE: + case WL_PLAN_EXPR_CMP_STR_EQ: + case WL_PLAN_EXPR_CMP_STR_NEQ: + case WL_PLAN_EXPR_CMP_STR_LT: + case WL_PLAN_EXPR_CMP_STR_GT: + case WL_PLAN_EXPR_CMP_STR_LTE: + case WL_PLAN_EXPR_CMP_STR_GTE: + case WL_PLAN_EXPR_STR_FN_CONTAINS: + case WL_PLAN_EXPR_STR_FN_STR_PREFIX: + case WL_PLAN_EXPR_STR_FN_STR_SUFFIX: + arity = 2; result = WIRELOG_TYPE_BOOL; break; + case WL_PLAN_EXPR_STR_FN_TO_UPPER: + case WL_PLAN_EXPR_STR_FN_TO_LOWER: + case WL_PLAN_EXPR_STR_FN_TRIM: + case WL_PLAN_EXPR_STR_FN_TO_STRING: + arity = 1; result = WIRELOG_TYPE_STRING; break; + case WL_PLAN_EXPR_STR_FN_CAT: + case WL_PLAN_EXPR_STR_FN_UUID5_RFC: + arity = 2; result = WIRELOG_TYPE_STRING; break; + case WL_PLAN_EXPR_STR_FN_SUBSTR: + case WL_PLAN_EXPR_STR_FN_STR_REPLACE: + arity = 3; result = WIRELOG_TYPE_STRING; break; + default: return false; /* No opaque, extension or aggregate contract. */ + } + if (depth < arity) return false; + depth -= arity; + if (depth == COL_FILTER_STACK) return false; + ++depth; + } + if (depth != 1) return false; + *type = result; + return true; +} + bool wl_columnar_expr_compiled_result_type(const wl_columnar_expr_compiled_t *c, const wirelog_column_type_t *source_types, uint32_t ncols, diff --git a/wirelog/columnar/internal.h b/wirelog/columnar/internal.h index fb2dd2316..1a21f4490 100644 --- a/wirelog/columnar/internal.h +++ b/wirelog/columnar/internal.h @@ -5310,6 +5310,12 @@ bool wl_columnar_expr_compiled_result_type( const wl_columnar_expr_compiled_t *c, const wirelog_column_type_t *source_types, uint32_t ncols, wirelog_column_type_t *type); +/* Pure static result query for complete canonical fixed-result scalar builtin + * expressions. No execution, operand/ID validation, intern mutation or callbacks. + * Unknown/extension/aggregate/malformed input leaves type untouched. */ +bool wl_columnar_expr_interpreted_result_type(const uint8_t *buf, uint32_t size, + const wirelog_column_type_t *source_types, uint32_t ncols, + wirelog_column_type_t *type); int wl_columnar_expr_eval_compiled(const wl_columnar_expr_compiled_t *c, const int64_t *row, uint32_t ncols, int64_t *out_val); diff --git a/wirelog/columnar/ops.c b/wirelog/columnar/ops.c index f295645fe..18d572cd9 100644 --- a/wirelog/columnar/ops.c +++ b/wirelog/columnar/ops.c @@ -241,7 +241,7 @@ wl_columnar_ops_reject_null_session(eval_stack_t *stack) return eval_stack_pop_relation(stack, &discarded); } -/* Complete schema only: an interpreter-only lane retains the legacy whole +/* Complete schema only: an unclassified lane retains the legacy whole * untyped output. Resolve without running any expression/callback, then install * on the still-empty private output using normal governed metadata ownership. * The temporary native vector is covered by the caller's scratch admission. */ @@ -261,6 +261,10 @@ wl_columnar_ops_map_set_types(const wl_plan_op_t *op, const col_rel_t *input, known = c < program_count && programs && wl_columnar_expr_compiled_result_type(programs[c], input->column_types, input->ncols, &types[c]); + if (!known) + known = wl_columnar_expr_interpreted_result_type( + op->map_exprs[c].data, op->map_exprs[c].size, + input->column_types, input->ncols, &types[c]); } else { uint32_t column = op->project_indices ? op->project_indices[c] : c; types[c] = input->column_types && column < input->ncols diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 4d7b6a098..19938a13a 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -654,8 +654,10 @@ wl_columnar_session_cold_projection_rule(const wl_col_session_t *s, types[i] != WIRELOG_TYPE_INT64 && types[i] != WIRELOG_TYPE_UINT32 && types[i] != WIRELOG_TYPE_UINT64 && - types[i] != WIRELOG_TYPE_FLOAT && types[i] != WIRELOG_TYPE_BOOL) - return false; + types[i] != WIRELOG_TYPE_FLOAT && + types[i] != WIRELOG_TYPE_BOOL && + !(types[i] == WIRELOG_TYPE_STRING && s->intern && + s->intern == s->plan->intern)) return false; } return wl_columnar_eval_plan_cold_projection(rp, name, width, types); } @@ -669,6 +671,11 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, { const wl_plan_t *plan = sess->plan; if (!plan || !name || !ncols || !plan->strata) return false; + bool references = false; + for (uint32_t i = 0; types && i < ncols; i++) + references |= types[i] == WIRELOG_TYPE_STRING; + if (references && + (!sess->intern || sess->intern != plan->intern)) return false; bool edb = false, consumer = false; for (uint32_t i = 0; i < plan->edb_count; i++) if (plan->edb_relations && plan->edb_relations[i] && @@ -685,7 +692,8 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, rp); bool selected_projection = projection && !strcmp(rp->ops[0].relation_name, name); - bool selected_join = rp->op_count && rp->ops[0].relation_name && + bool selected_join = !references && rp->op_count && + rp->ops[0].relation_name && !strcmp(rp->ops[0].relation_name, name) && wl_columnar_join_cold_prefix(sess, rp); if (selected_projection || selected_join) consumer = true; @@ -706,7 +714,8 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, if (!op->right_relation || strcmp(op->right_relation, name) != 0) continue; - if (op->op != WL_PLAN_OP_JOIN || op->key_count != 1 || + if (references || op->op != WL_PLAN_OP_JOIN || + op->key_count != 1 || op->right_filter_expr.size || (op->project_count && !op->project_indices)) return false; int key = wl_columnar_relation_schema_column_index(ncols, @@ -746,6 +755,10 @@ wl_columnar_session_cold_execution_check(const wl_col_session_t *s) c = c->next) { const wirelog_column_type_t *types = wl_columnar_batch_payload_schema( c->payload, NULL, NULL); + for (uint32_t i = 0; i < c->ncols; i++) + if (types[i] == WIRELOG_TYPE_STRING && + !wl_columnar_batch_payload_uses_intern(c->payload, + s->intern)) return ENOTSUP; if (!wl_columnar_session_cold_plan(s, c->name, c->ncols, (const char *const *)c->col_names, types)) return ENOTSUP; } @@ -854,9 +867,11 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, c->cold_storage_generation = r->storage_generation + 1; bool inserted_name = s->last_inserted_relation && strcmp(s->last_inserted_relation, r->name) == 0; - wl_columnar_spill_status_t rc = wl_columnar_batch_payload_capture( + wl_columnar_spill_status_t rc = + wl_columnar_batch_payload_capture_with_intern( s->memory_governor, s->spill_store, r, r->nrows, - WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, cancel, context, &c->payload); + WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, s->intern, cancel, context, + &c->payload); if (rc == WL_COLUMNAR_SPILL_OK && cancel && cancel(context)) rc = WL_COLUMNAR_SPILL_CANCELLED; if (rc == WL_COLUMNAR_SPILL_OK) { @@ -4413,17 +4428,19 @@ col_session_create_internal(const wl_plan_t *plan, uint32_t num_workers, /* Preserve declared physical lane types before the first fact is * inserted. In particular, float facts are binary64 bits in the * existing 64-bit storage lane and must not be accepted as legacy - * integers merely because the relation starts empty. */ - bool has_float_type = false; + * integers merely because the relation starts empty. STRING IDs also + * require their declared type before raw insertion or cold capture. */ + bool has_declared_lane_type = false; if (plan->edb_column_types && plan->edb_column_type_counts && plan->edb_column_types[i] && plan->edb_column_type_counts[i] > 0) { uint32_t type_count = plan->edb_column_type_counts[i]; for (uint32_t c = 0; c < type_count; c++) - has_float_type |= plan->edb_column_types[i][c] - == WIRELOG_TYPE_FLOAT; + has_declared_lane_type |= plan->edb_column_types[i][c] + == WIRELOG_TYPE_FLOAT || plan->edb_column_types[i][c] + == WIRELOG_TYPE_STRING; } - if (has_float_type) { + if (has_declared_lane_type) { uint32_t type_count = plan->edb_column_type_counts[i]; rc = col_rel_set_schema(r, type_count, NULL); if (rc == 0) diff --git a/wirelog/columnar/spill.c b/wirelog/columnar/spill.c index 337854d7f..0e9ebe6bb 100644 --- a/wirelog/columnar/spill.c +++ b/wirelog/columnar/spill.c @@ -281,6 +281,14 @@ wl_columnar_spill_store_retire(wl_columnar_spill_store_t *store) store->retired = true; } +bool +wl_columnar_spill_segment_uses_intern( + const wl_columnar_spill_segment_t *segment, + const wl_intern_t *intern) +{ + return segment && intern && segment->intern == intern; +} + bool wl_columnar_spill_store_uses_governor(const wl_columnar_spill_store_t *store, const wl_columnar_memory_governor_ref_t *governor) diff --git a/wirelog/columnar/spill.h b/wirelog/columnar/spill.h index aa350cf05..41e154aec 100644 --- a/wirelog/columnar/spill.h +++ b/wirelog/columnar/spill.h @@ -85,6 +85,11 @@ wl_columnar_spill_status_t wl_columnar_spill_store_close( * callback context or compound arena; STRING/INLINE segments additionally pin intern. */ void wl_columnar_spill_store_retire(wl_columnar_spill_store_t *store); /* Live-store identity check for adapters sharing admission with the store. */ +/* Identity only, over a live segment owner/reader capability. Does not acquire + * a new pin or claim that the original intern owner is still open. */ +bool wl_columnar_spill_segment_uses_intern( + const wl_columnar_spill_segment_t *segment, const wl_intern_t *intern); + bool wl_columnar_spill_store_uses_governor( const wl_columnar_spill_store_t *store, const wl_columnar_memory_governor_ref_t *governor); diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index 4d8a961fb..d072d721d 100644 --- a/wirelog/wirelog-advanced.h +++ b/wirelog/wirelog-advanced.h @@ -214,7 +214,7 @@ wirelog_session_create_with_options(wirelog_program_t *program, const wirelog_session_options_v1_t *options, wirelog_session_t **out); /** - * Seal one numeric input as persistent immutable spill input before + * Seal one eligible input as persistent immutable spill input before * the first STEP/SNAPSHOT attempt. Load all inputs first. Requires configured * spill JOIN caps and a serial nonrecursive plan using only audited * VARIABLE/JOIN/MAP/CONSOLIDATE operations and the restricted FILTER below. @@ -228,8 +228,12 @@ wirelog_session_create_with_options(wirelog_program_t *program, * A cold left input is consumed by an initial VARIABLE/JOIN pair; both sides * may be sealed, including a self-JOIN. Subsequent JOINs consume an admitted * resident intermediate. No FILTER/MAP before that first cold JOIN, multi-key, - * cross/theta JOIN, non-INT64 key, right filter, timestamps, references, + * cross/theta JOIN, non-INT64 key, right filter, timestamps, compound values, * opaque/materialized rewrites, worker/control/delta modes or delta callback. + * Standalone direct/filtered projections also accept STRING lanes from the + * originating program intern table; reference-bearing JOINs remain unsupported. + * The program must outlive the session. Intern storage remains resident, and + * sealing validates IDs even in unprojected STRING lanes. * Optimized plans may therefore be ineligible. The session must also not have * been created with WIRELOG_JOIN_BATCH_BYTES on (see * wirelog_session_options_v1_t). Multiple loaded inputs may be From 090f2ce999da2224e14b82fa355c0893fbccc14e Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 18:44:07 +0900 Subject: [PATCH 5/9] feat(columnar): join sealed STRING inputs --- CHANGELOG.md | 7 +- docs/MEMORY.md | 21 ++- docs/SEMANTICS.md | 10 +- tests/test_payload_join.c | 173 +++++++++++++++++++++++ tests/test_session_cold_relation.c | 205 ++++++++++++++++++++++++++++ tests/test_wirelog_advanced_spill.c | 74 ++++++++++ wirelog/columnar/join.c | 64 +++++++-- wirelog/columnar/payload_join.c | 47 +++++-- wirelog/columnar/payload_join.h | 16 +++ wirelog/columnar/session.c | 7 +- wirelog/wirelog-advanced.h | 9 +- 11 files changed, 591 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3cc1bb6b..b77e0c4e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,15 @@ All notable changes to wirelog are documented in this file. ### Added +- **STRING cold JOIN consumers** (#1372): matching STRING or INT64 keys, + STRING payloads, both cold directions, self JOIN and resident intermediate + chains preserve originating intern identity and independently pinned output. + Fixed-intern scratch and retained-source controls keep memory floors explicit. + - **STRING cold projections** (#1372): standalone direct/filtered projections consume sealed STRING lanes with actual types, originating-intern checks and pinned storage. Multiple bindings, failure/retry and fixed-budget tests retain - explicit resident intern/output floors; reference JOINs remain follow-up work. + explicit resident intern/output floors. - **Sealed JOIN inputs in either position** (#1372): bounded readers now consume cold left, both-cold and self-JOIN inputs. Audited chains continue through diff --git a/docs/MEMORY.md b/docs/MEMORY.md index d3ab298df..e1c88809d 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -4984,7 +4984,7 @@ credit. The persistent binding stays session-owned and valid for permitted retries. This does not promise rollback of other heads already committed by an earlier STEP relation, or add asynchronous cancellation/control support. -### STRING cold projection consumers (#1372, partial) +### STRING cold projection and JOIN consumers (#1372, partial) Standalone direct and narrowly filtered projections accept actual STRING lanes, including reordered and repeated outputs and multiple sealed inputs. Source @@ -4992,10 +4992,15 @@ initialization preserves declared STRING types even for raw host inserts and empty relations. Capture validates every STRING ID, including unused lanes, and pins the originating session/program intern table. Runtime checks compare table identity through the retained payload or spill-segment owner; a different -table with coincident IDs is not equivalent. Existing numeric JOIN routes remain -numeric-only in this increment; reference-bearing JOINs are refused. - -Projection preserves STRING types and IDs without rehydrating full input rows. +table with coincident IDs is not equivalent, even under the same governor. +The reference-aware JOIN route accepts matching INT64 or STRING keys and +STRING payload columns in either input. Legacy numeric-only payload producer +constructors retain their restrictions. Self-JOIN and chained consumers use +independent reader pins; a stored output payload acquires its own intern pin +before batch acknowledgement. An owned reference intermediate is captured +before checked retirement, preserving the same resident intermediate floor. + +Projection and JOIN preserve STRING types and IDs without rehydrating full input rows. The program must outlive public sessions; output IDs borrow that lifetime. Payload readers retain source storage while live, including after owner retirement. Independently retained output requires its own intern capability. @@ -5003,11 +5008,13 @@ Intern strings/table storage remain resident and charged. Tests pair reclaimed and retained STRING sources under one allowance and separately exhaust real admission with increasing distinct strings; spilling rows cannot remove this floor. Private output/read/allocation failures preserve the sealed binding and -release attempt resources before retry. +release attempt resources before retry. Fixed-intern two-cardinality tests +keep JOIN scratch unchanged under fixed index/batch caps; retained-source +controls cover one and both input owners without hiding resident output costs. ### Remaining #1372 implementation groups -The sealed numeric JOIN and scalar STRING projection subsets now have actual persistent cold storage, +The sealed scalar numeric/STRING JOIN and projection subsets now have actual persistent cold storage, resident reclamation and evaluator consumers. General cold VARIABLE/direct-column access beyond the exact direct-projection subset, mutable relations, persistent trace/differential storage and their arrangement consumers remain separate integration work. They require stable diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index 3a87dd609..d19ef1bfb 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -259,9 +259,9 @@ INT64 column with one integer literal (`=`, `!=`, `<`, `>`, `<=`, `>=`), with th column on the left. Other predicates and arbitrary cold expressions remain unsupported. An initial `VARIABLE → JOIN` may instead consume a cold left source, including two sealed sources or the same binding on both sides. -Each JOIN uses one INT64 equality key. Chained JOINs continue from an admitted +Each JOIN uses one matching INT64 or STRING equality key. Chained JOINs continue from an admitted resident intermediate; FILTER/MAP before the first cold JOIN, multi-key/cross/ -theta JOINs, non-INT64 keys and right filters remain unsupported. An eligible direct/filtered projection alone is sufficient; a +theta JOINs, other key types and right filters remain unsupported. An eligible direct/filtered projection alone is sufficient; a JOIN consumer is not required. Reads use independent bounded pins; projected output and consolidation remain resident. Output capacity is admitted for the full source row count even if no row passes. @@ -307,8 +307,10 @@ not assume symmetric coverage. Standalone direct/filtered cold projections also accept STRING lanes from the originating program's intern table, preserving STRING output types and IDs. Every STRING lane is validated at capture, including unprojected lanes. Empty -strings and non-ASCII strings retain their ordinary intern identity. Reference -JOINs and compound sources remain unsupported. Pins retain backing storage; the +strings and non-ASCII strings retain their ordinary intern identity. JOINs +also preserve STRING payload lanes and compare STRING keys only after both +inputs prove the same originating intern table. Coincident IDs from another +table are refused; compound sources remain unsupported. Pins retain backing storage; the public program must still outlive its sessions, and intern storage is a resident memory floor. A copied raw ID alone does not extend that lifetime. diff --git a/tests/test_payload_join.c b/tests/test_payload_join.c index ab7d67712..fb07f3c76 100644 --- a/tests/test_payload_join.c +++ b/tests/test_payload_join.c @@ -1359,8 +1359,181 @@ static void projected(void) } } +static col_rel_t *reference_source(wl_intern_t *intern, unsigned rows, + bool string_key) +{ + const char *values[] = {"", "alpha", "한글"}; + int64_t ids[3]; + for (unsigned i = 0; i < 3; i++) { + ids[i] = wl_intern_put(intern, values[i]); CHECK(ids[i] >= 0); + } + col_rel_t *r = col_rel_new_auto("reference", 3); CHECK(r); + wirelog_column_type_t types[] = + {string_key ? WIRELOG_TYPE_STRING : WIRELOG_TYPE_INT64, WIRELOG_TYPE_INT64, + WIRELOG_TYPE_STRING}; + CHECK(col_rel_set_column_types(r, types, 3) == 0); + for (unsigned i = 0; i < rows; i++) { + int64_t row[] = {string_key?ids[i%3]:(int64_t)(i%3), i, ids[(i+1)%3]}; + CHECK(col_rel_append_row(r, row) == 0); + } + return r; +} +static wl_columnar_batch_payload_t *reference_capture( + wl_columnar_memory_governor_ref_t *g, + wl_columnar_spill_store_t *store, col_rel_t *r, wl_intern_t *intern, + unsigned mode) +{ + wl_columnar_batch_payload_t *p = NULL; + CHECK(wl_columnar_batch_payload_capture_with_intern(g, store, r, r->nrows, + mode == + 1 ? WL_COLUMNAR_BATCH_PAYLOAD_SPILLED : + WL_COLUMNAR_BATCH_PAYLOAD_RESIDENT, + intern, NULL, NULL, &p) == OK); + if (mode == 2)CHECK(wl_columnar_batch_payload_spill(&p, store, NULL, + NULL) == OK); + return p; +} +static void reference_joins(void) +{ + const uint32_t map[] = {5, 1, 4, 2, 5}; + const char *values[] = {"", "alpha", "한글"}; + for (unsigned string_key = 0; string_key < 2; + string_key++) for (unsigned mode = 0; + mode < 5; mode++) { + wl_columnar_memory_governor_ref_t *g = governor(1<<20); + wl_intern_t *intern = wl_intern_create(); + CHECK(intern && wl_intern_attach_memory_governor(intern, g) == 0); + wl_columnar_spill_store_t *store; + CHECK(wl_columnar_spill_store_open(test_tmpdir(), g, 65536, 64, + &store) == OK); + col_rel_t *l = reference_source(intern, 9, string_key), + *r = reference_source(intern, 11, string_key); + wl_columnar_batch_payload_t *lp = reference_capture(g, store, l, + intern, mode == 4?2:mode%2); + wl_columnar_batch_payload_t *rp = reference_capture(g, store, r, + intern, (mode/2)%2); + col_rel_destroy(l); col_rel_destroy(r); + uint64_t baseline = reserved(g); + wl_columnar_continuation_t *join = NULL; + CHECK(create_join(g, lp, rp, 0, 0, 48, NULL, NULL, + &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); + CHECK(create_weighted_join(g, lp, rp, 0, 0, 48, NULL, NULL, + &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); + CHECK(wl_columnar_payload_join_create_projected_bounded(g, lp, rp, + 0, 0, map, 5, 40, index_cap(1), NULL, NULL, + &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); + CHECK(wl_columnar_payload_join_create_reference_projected_bounded(g, + lp, rp, 0, 0, map, 5, 40, index_cap(1), NULL, NULL, NULL, + &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); + wl_intern_t *foreign = wl_intern_create(); + CHECK(foreign && wl_intern_attach_memory_governor(foreign, g) == 0); + CHECK(wl_intern_put(foreign, "different-at-zero") == 0); + col_rel_t *f = reference_source(foreign, 3, string_key); + wl_columnar_batch_payload_t *fp = reference_capture(g, store, f, + foreign, 1); col_rel_destroy(f); + CHECK(wl_columnar_payload_join_create_reference_projected_bounded(g, + lp, fp, 0, 0, map, 5, 40, index_cap(1), intern, NULL, NULL, + &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); + CHECK(wl_columnar_batch_payload_release(fp) == OK); + wl_intern_free(foreign); + f = reference_source(intern, 3, !string_key); + fp = reference_capture(g, store, f, intern, 0); col_rel_destroy(f); + CHECK(wl_columnar_payload_join_create_reference_projected_bounded(g, + lp, fp, 0, 0, map, 5, 40, index_cap(1), intern, NULL, NULL, + &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); + CHECK(wl_columnar_batch_payload_release(fp) == OK && + reserved(g) == baseline); + for (unsigned fault = 1; fault <= 3; fault++) { + allocation_fail = fault; + CHECK( + wl_columnar_payload_join_create_reference_projected_bounded( + g, lp, rp, 0, 0, map, 5, 40, index_cap(1), intern, NULL, + NULL, &join) == WL_COLUMNAR_SPILL_ALLOCATION && !join); + CHECK(reserved(g) == baseline); + } + CHECK(wl_columnar_payload_join_create_reference_projected_bounded(g, + lp, rp, 0, 0, map, 5, 40, index_cap(mode?3:1), intern, NULL, + NULL, &join) == OK); + sink_t sink_state; wl_columnar_continuation_sink_t sink; + sink_init(&sink_state, &sink, g); + wl_columnar_join_batch_payload_sink_init_with_intern( + &sink_state.owned, &sink_state.inner, g, NULL, + WL_COLUMNAR_BATCH_PAYLOAD_RESIDENT, intern); + sink_state.failure = mode; + bool seen[9][11] = {{false}}; unsigned count = 0, calls = 0; + wl_columnar_batch_payload_t *retained = NULL; + for (;;) { + CHECK(++calls < 10000); + wl_columnar_continuation_status_t status = + wl_columnar_continuation_publish(join, &sink); + if (status == WL_COLUMNAR_CONTINUATION_PROGRESS) continue; + if (status == WL_COLUMNAR_CONTINUATION_SINK_FAILURE || + status == WL_COLUMNAR_CONTINUATION_COMMIT_FAILURE || + status == WL_COLUMNAR_CONTINUATION_RESERVATION_DENIED) { + CHECK(mode == 1 || mode == 2 || mode == 4); + CHECK(wl_columnar_batch_payload_release(lp) == + WL_COLUMNAR_SPILL_BUSY); + sink_state.failure = 0; continue; + } + CHECK(status == WL_COLUMNAR_CONTINUATION_OK || + status == WL_COLUMNAR_CONTINUATION_COMMIT_AMBIGUOUS || + status == WL_COLUMNAR_CONTINUATION_DONE); + sink_state.failure = 0; + wl_columnar_batch_payload_t *out = + wl_columnar_join_batch_payload_sink_take(&sink_state.owned); + if (out) { + uint32_t cols, rows; + const wirelog_column_type_t *types = + wl_columnar_batch_payload_schema(out, &cols, &rows); + CHECK(cols == 5 && rows == 1 && + types[0] == WIRELOG_TYPE_STRING && + types[1] == WIRELOG_TYPE_INT64 && + types[2] == WIRELOG_TYPE_INT64 && + types[3] == WIRELOG_TYPE_STRING && + types[4] == WIRELOG_TYPE_STRING); + CHECK(wl_columnar_batch_payload_uses_intern(out, intern)); + wl_columnar_batch_payload_reader_t *reader; + CHECK(wl_columnar_batch_payload_acquire(out, + &reader) == OK); + const uint64_t *lanes; wl_columnar_spill_metadata_t meta; + CHECK(wl_columnar_batch_payload_read(reader, 0, NULL, NULL, + &lanes, &meta) == OK); + unsigned i = (unsigned)lanes[1], j = (unsigned)lanes[2]; + CHECK(i < 9 && j < 11 && i%3 == j%3 && !seen[i][j]); + seen[i][j] = true; count++; + CHECK(!strcmp(wl_intern_reverse(intern, (int64_t)lanes[0]), + values[(j+1)%3]) && !strcmp(wl_intern_reverse(intern, + (int64_t)lanes[3]), + values[(i+1)%3]) && lanes[0] == lanes[4]); + wl_columnar_batch_payload_reader_release(reader); + if (!retained) retained = out; + else CHECK(wl_columnar_batch_payload_release(out) == OK); + } + if (status == WL_COLUMNAR_CONTINUATION_DONE) break; + } + CHECK(count == 33 && retained); + wl_columnar_continuation_destroy(join); + CHECK(wl_columnar_batch_payload_release(lp) == OK && + wl_columnar_batch_payload_release(rp) == OK); + CHECK(wl_columnar_spill_store_close(store) == OK); + wl_intern_free(intern); /* Independently captured output retains its own pin. */ + wl_columnar_batch_payload_reader_t *reader; + CHECK(wl_columnar_batch_payload_acquire(retained, &reader) == OK); + const uint64_t *lanes; wl_columnar_spill_metadata_t meta; + CHECK(wl_columnar_batch_payload_read(reader, 0, NULL, NULL, &lanes, + &meta) == OK); + CHECK(wl_intern_reverse(intern, + (int64_t)lanes[0]) && wl_intern_reverse(intern, + (int64_t)lanes[3])); + wl_columnar_batch_payload_reader_release(reader); + CHECK(wl_columnar_batch_payload_release(retained) == OK); + CHECK(reserved(g) == 0); wl_columnar_memory_governor_ref_release(g); + } +} + int main(void) { + reference_joins(); projected(); combinations(); failures(); budget(false); budget(true); misses(); concurrent_self_join(false); concurrent_self_join(true); diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 5cef9cebf..3af80450e 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -2299,6 +2299,209 @@ static void reference_projection_memory(void) destroy(&f); } +typedef struct { + wl_col_session_t *session; + unsigned count; + bool seen[144]; + bool numeric; +} reference_join_events_t; +static void reference_join_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + reference_join_events_t *e = context; + CHECK(!strcmp(name, "result") && width == 5); + CHECK(row[1] >= 0 && row[1] < 12 && row[3] >= 0 && row[3] < 12); + unsigned x = (unsigned)row[1], y = (unsigned)row[3]; + CHECK(x % 3 == y % 3); + unsigned pair = x*12+y; + CHECK(!e->seen[pair]); e->seen[pair] = true; e->count++; + if (e->numeric) CHECK(row[0] == (int64_t)(x%3)); + else CHECK(!strcmp(wl_intern_reverse(e->session->intern, row[0]), + reference_values[x%3])); + CHECK(!strcmp(wl_intern_reverse(e->session->intern, row[2]), + reference_values[(x+1)%3])); + CHECK(!strcmp(wl_intern_reverse(e->session->intern, row[4]), + reference_values[(y+2)%3])); +} +static void reference_join_directions(void) +{ + for (unsigned numeric = 0; numeric < 2; numeric++) + for (unsigned shape = 0; shape < 5; shape++) + for (unsigned direction = 0; direction < 5; direction++) { + char text[512]; + snprintf(text, sizeof(text), + ".decl a(k:%s,x:int64,p:symbol)\n" + ".decl b(k:%s,y:int64,q:symbol)\n" + ".decl result(k:%s,x:int64,p:symbol,y:int64,q:symbol)\n" + "result(k,x,p,y+0,q) :- a(k,x,p),b(k,y,q).\n", + numeric ? "int64" : "symbol", numeric ? "int64" : "symbol", + numeric ? "int64" : "symbol"); + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + int64_t ids[3]; + for (unsigned i = 0; i < 3; i++) + ids[i] = wl_intern_put(s->intern, reference_values[i]); + int64_t absent = numeric ? 99 : wl_intern_put(s->intern, + "absent"); + wl_plan_relation_t *rp = join_body(&f, "result"); + CHECK(rp->ops[0].op == WL_PLAN_OP_VARIABLE && + !strcmp(rp->ops[0].relation_name, "a") && + rp->ops[1].op == WL_PLAN_OP_JOIN); + for (unsigned i = 0; i < 12; i++) { + int64_t a[] = {numeric ? (int64_t)(i%3) : ids[i%3], i, + ids[(i+1)%3]}; + int64_t b[] = {shape == 1 ? absent : a[0], i, ids[(i+2)%3]}; + if (shape != 2 && shape != 4) + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + if (shape != 3 && shape != 4) + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + } + seal_direction(&f, direction); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + if (direction && !shape) { + unsigned count = 0; read_fault = 7; + CHECK(wl_session_snapshot(f.session, count_only, + &count) == EIO && !count); + read_fault = 0; + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == + disk); + } + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_join_events_t e = {.session = s, + .numeric = numeric != 0}; + CHECK(wl_session_snapshot(f.session, reference_join_observe, + &e) == 0); + CHECK(e.count == (shape ? 0 : 48)); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == + disk); + if (!shape) { + col_rel_t *out = session_find_rel(s, "result"); + CHECK(out && out->column_types && + out->column_types[2] == WIRELOG_TYPE_STRING && + out->column_types[4] == WIRELOG_TYPE_STRING); + } + CHECK(wl_session_step(f.session) == 0); + } + destroy(&f); + } +} + +static const char *reference_join_budget_query = + ".decl a(k:int64,v:int64,s:symbol)\n.decl b(k:int64,v:int64,t:symbol)\n" + ".decl result(k:int64,x:int64,y:int64,s:symbol)\n" + "result(k,x,y+0,s) :- a(k,x,s),b(k,y,t).\n"; +static fixture_t reference_join_budget_fixture(uint64_t allowance, bool both) +{ + fixture_t f = fixture(reference_join_budget_query, allowance, 8*1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_intern_put(s->intern, "shared") == 0); + s->spill_join_batch_bytes = 1024; s->spill_join_index_bytes = 64*1024; + for (int64_t i = 0; i < 4096; i++) { + int64_t a[] = {i < 128 ? 0 : 10000+i, i, 0}; + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + } + for (int64_t i = 0; i < (both ? 4096 : 128); i++) { + int64_t b[] = {i < 128 ? 0 : 20000+i, 100+i, 0}; + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + } + return f; +} +typedef struct { unsigned count; uint64_t seen[256]; +} reference_reclaimed_events_t; +static void reference_reclaimed_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + reference_reclaimed_events_t *e = context; + CHECK(!strcmp(name, + "result") && width == 4 && row[3] == 0 && row[0] == 0 && row[1] >= 0 && + row[1] < 128 && + row[2] >= 100 && row[2] < 228); + unsigned pos = (unsigned)(row[1]*128 + row[2]-100); + uint64_t bit = UINT64_C(1) << (pos%64); + CHECK(!(e->seen[pos/64]&bit)); e->seen[pos/64] |= bit; e->count++; +} +static void reference_join_reclamation(void) +{ + wl_columnar_memory_governor_test_admitted = budget_admitted; + for (unsigned both = 0; both < 2; both++) { + budget_peak = 0; + fixture_t calibration = reference_join_budget_fixture(8*1024*1024, + both != 0); + seal_direction(&calibration, both ? 3 : 1); + uint64_t preparation = budget_peak; + reference_reclaimed_events_t e = {0}; + CHECK(wl_session_snapshot(calibration.session, + reference_reclaimed_observe, + &e) == 0 && e.count == 16384); + uint64_t allowance = budget_peak; + CHECK(allowance > preparation); + destroy(&calibration); + for (unsigned retain = 0; retain < 2; retain++) { + budget_peak = 0; + fixture_t f = reference_join_budget_fixture(allowance, both != 0); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f); + wl_columnar_session_test_retain_cold_source = retain != 0; + seal_direction(&f, both ? 3 : 1); + wl_columnar_session_test_retain_cold_source = false; + CHECK(retain ? reserved(&f) > before : reserved(&f) < before); + unsigned owners = 0; + for (wl_columnar_session_cold_relation_t *c = s->cold_relation; c; + c = c->next) { + CHECK((c->retained_source != NULL) == (retain != 0)); owners++; + } + CHECK(owners == (both ? 2u : 1u)); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + e = (reference_reclaimed_events_t){0}; + int rc = wl_session_snapshot(f.session, reference_reclaimed_observe, + &e); + CHECK(budget_peak <= allowance); + if (retain) CHECK(rc == ENOSPC && + wl_session_budget_denied(f.session) && e.count == 0); + else CHECK(rc == 0 && e.count == 16384); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + CHECK(!wl_columnar_session_test_has_retained_cold_source(NULL)); + } + } + wl_columnar_memory_governor_test_admitted = NULL; +} +static void reference_join_bounded_scratch(void) +{ + uint64_t peaks[2] = {0}; + for (unsigned large = 0; large < 2; large++) { + fixture_t f = fixture(reference_join_budget_query, 4*1024*1024, + 1024*1024); + CHECK(wl_intern_put(COL_SESSION(f.session)->intern, "shared") == 0); + for (int64_t i = 0; i < (large ? 512 : 64); i++) { + int64_t a[] = {i, 10+i, 0}, b[] = {10000+i, 100+i, 0}; + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + } + seal_direction(&f, 3); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + budget_peak = before; constructor_peak = 0; + wl_columnar_memory_governor_test_admitted = budget_admitted; + wl_columnar_join_test_spill_producer_result = constructor_result; + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_join_cold_left(s, join_body(&f, "result"), + &stack) == 0); + peaks[large] = constructor_peak-before; + CHECK(constructor_peak > before && budget_peak == constructor_peak); + eval_entry_t e; + CHECK(eval_stack_pop_relation(&stack, &e) == 0 && e.rel->nrows == 0); + CHECK(eval_stack_dispose_entry(&stack, + &e) == 0 && reserved(&f) == before); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + wl_columnar_memory_governor_test_admitted = NULL; + wl_columnar_join_test_spill_producer_result = NULL; + destroy(&f); + } + CHECK(peaks[0] == peaks[1]); +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -2308,6 +2511,8 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + reference_join_reclamation(); reference_join_bounded_scratch(); + reference_join_directions(); reference_projection_memory(); reference_projections(); reference_projection_failures(); reference_projection_bytecode(); diff --git a/tests/test_wirelog_advanced_spill.c b/tests/test_wirelog_advanced_spill.c index cd79da03f..bfe73b1a1 100644 --- a/tests/test_wirelog_advanced_spill.c +++ b/tests/test_wirelog_advanced_spill.c @@ -837,6 +837,79 @@ static void reference_public_projections(void) } } +typedef struct { uint64_t ids[3]; unsigned seen[4]; } reference_chain_events_t; +static void reference_chain_observe(const char *name, + const wirelog_typed_row_v1_t *row, int32_t diff, void *context) +{ + reference_chain_events_t *e = context; + unsigned which = !strcmp(name, "result") ? 0 : !strcmp(name, "self") ? 1 : + !strcmp(name, "scan") ? 2 : 3; + CHECK(which != 3 || !strcmp(name, "second")); + unsigned width = which == 2 ? 2 : 4; + CHECK(diff == 1 && row->logical_ncols == width && + row->physical_nlanes == width); + unsigned i = (unsigned)row->lanes[1]; CHECK(i < 3); + CHECK(row->types[0] == WIRELOG_TYPE_STRING && + row->types[1] == WIRELOG_TYPE_INT64); + CHECK(row->physical_types[0] == WIRELOG_TYPE_STRING && + row->lanes[0] == e->ids[i]); + if (which != 2) { + CHECK(row->types[2] == WIRELOG_TYPE_STRING && + row->types[3] == WIRELOG_TYPE_INT64); + CHECK(row->physical_types[2] == WIRELOG_TYPE_STRING); + CHECK(row->lanes[2] == e->ids[which == 1 ? i : (i+1)%3]); + CHECK(row->lanes[3] == i + (which == 0 ? 100u : which == 1 ? 0u : 10u)); + } + CHECK(!(e->seen[which] & (1u<seen[which] |= 1u<= 3) for (unsigned i = 0; i < 3; i++) + CHECK(wirelog_session_seal_input(s, + names[direction == 3 ? i : 2-i]) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_chain_events_t e = expected; + CHECK(wirelog_session_snapshot_typed(s, reference_chain_observe, + &e) == WIRELOG_OK); + for (unsigned i = 0; i < 4; i++) CHECK(e.seen[i] == 7); + CHECK(wirelog_session_step(s) == WIRELOG_OK); + } + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + int main(void) { /* Issue #2287: a session created with a nonzero WIRELOG_JOIN_BATCH_BYTES @@ -855,6 +928,7 @@ int main(void) test_tmpdir()); CHECK(mkdtemp(directory)); #endif + reference_public_joins(); reference_public_projections(); join_directions(); join_chains(); descriptors(); lifecycle(); refusals(); typed_projection(); inline_facts(); diff --git a/wirelog/columnar/join.c b/wirelog/columnar/join.c index 4bd085a6d..a8a4de05d 100644 --- a/wirelog/columnar/join.c +++ b/wirelog/columnar/join.c @@ -1498,6 +1498,7 @@ typedef struct { uint32_t width; const char *const *names; const wirelog_column_type_t *types; + const wl_intern_t *intern; } wl_columnar_join_source_t; static wl_columnar_join_source_t @@ -1505,6 +1506,7 @@ wl_columnar_join_source(const wl_col_session_t *s, const char *name, col_rel_t *resident) { wl_columnar_join_source_t src = {0}; + src.intern = s->plan && s->intern == s->plan->intern ? s->intern : NULL; src.cold = wl_columnar_session_cold_find(s, name); if (src.cold) { src.width = src.cold->ncols; @@ -1537,12 +1539,35 @@ wl_columnar_join_source_key(const wl_columnar_join_source_t *src, return key < 0 ? 0 : (uint32_t)key; } +static bool +wl_columnar_join_source_references(const wl_columnar_join_source_t *src) +{ + for (uint32_t c = 0; src->types && c < src->width; c++) + if (src->types[c] == WIRELOG_TYPE_STRING) return true; + return false; +} + static bool wl_columnar_join_source_valid(const wl_columnar_join_source_t *src, uint32_t key) { - return src->cold ? key < src->width && src->types[key] == WIRELOG_TYPE_INT64 - : src->resident && wl_columnar_join_spill_schema(src->resident, key); + if (key >= src->width || (!src->cold && !src->resident)) return false; + const col_rel_t *r = src->resident; + if (r && (r->timestamps || r->has_graph_column || + r->compound_kind != WIRELOG_COMPOUND_KIND_NONE || r->compound_count || + r->compound_arity_map || r->compound_arity_len || + r->inline_physical_offset)) return false; + wirelog_column_type_t type = + src->types ? src->types[key] : WIRELOG_TYPE_INT64; + if (type != WIRELOG_TYPE_INT64 && type != WIRELOG_TYPE_STRING) return false; + for (uint32_t c = 0; src->types && c < src->width; c++) { + type = src->types[c]; + if (type < WIRELOG_TYPE_INT32 || type > WIRELOG_TYPE_BOOL) return false; + if (type == WIRELOG_TYPE_STRING && (!src->intern || (src->cold && + !wl_columnar_batch_payload_uses_intern(src->cold->payload, + src->intern)))) return false; + } + return true; } static bool @@ -1570,6 +1595,11 @@ wl_columnar_join_cold_shape(const wl_plan_op_t *op, /* Missing right retains ordinary empty-result/projection typing semantics. */ if (!right->cold && !right->resident) return true; if (!wl_columnar_join_source_valid(right, rk)) return false; + wirelog_column_type_t lt = + left->types ? left->types[lk] : WIRELOG_TYPE_INT64; + wirelog_column_type_t rt = + right->types ? right->types[rk] : WIRELOG_TYPE_INT64; + if (lt != rt) return false; uint64_t joined = (uint64_t)left->width + right->width; if (joined > UINT32_MAX) return false; for (uint32_t i = 0; i < op->project_count; i++) @@ -1694,25 +1724,37 @@ wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, continue; } wl_columnar_spill_status_t status; - if (i == 0 && left->owned && - wl_columnar_batch_payload_can_take(left->rel)) + bool transfer = i == 0 && left->owned && + wl_columnar_batch_payload_can_take(left->rel); + if (transfer && !wl_columnar_join_source_references(&sources[i])) status = wl_columnar_batch_payload_take(sess->memory_governor, sess->spill_store, &left->rel, NULL, NULL, &inputs[i]); - else - status = wl_columnar_batch_payload_capture(sess->memory_governor, - sess->spill_store, sources[i].resident, - sources[i].resident->nrows, - WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, NULL, NULL, &inputs[i]); + else { + status = + wl_columnar_batch_payload_capture_with_intern( + sess->memory_governor, + sess->spill_store, sources[i].resident, + sources[i].resident->nrows, + WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, sess->intern, NULL, NULL, + &inputs[i]); + if (status == WL_COLUMNAR_SPILL_OK && transfer) { + int retired = col_rel_destroy_checked(left->rel); + if (!retired) left->rel = NULL; + else status = retired == + EBUSY ? WL_COLUMNAR_SPILL_BUSY : + WL_COLUMNAR_SPILL_INVALID; + } + } owned[i] = inputs[i] != NULL; rc = wl_columnar_join_spill_error(sess, status); if (rc) goto done; } wl_columnar_spill_status_t status = - wl_columnar_payload_join_create_projected_bounded( + wl_columnar_payload_join_create_reference_projected_bounded( sess->memory_governor, inputs[0], inputs[1], lk, rk, op->project_indices, op->project_count, sess->spill_join_batch_bytes, sess->spill_join_index_bytes, - NULL, NULL, &cont); + sess->intern, NULL, NULL, &cont); #ifdef WL_TEST_JOIN_CACHE_HOOK if (wl_columnar_join_test_spill_producer_result) wl_columnar_join_test_spill_producer_result(status); diff --git a/wirelog/columnar/payload_join.c b/wirelog/columnar/payload_join.c index 771e7f846..824200cd4 100644 --- a/wirelog/columnar/payload_join.c +++ b/wirelog/columnar/payload_join.c @@ -289,17 +289,21 @@ wl_columnar_payload_join_produce(void *context, static wl_columnar_spill_status_t wl_columnar_payload_join_schema(wl_columnar_batch_payload_t *payload, - uint32_t key, uint32_t *cols, uint32_t *rows) + uint32_t key, uint32_t *cols, uint32_t *rows, const wl_intern_t *intern) { const wirelog_column_type_t *types = wl_columnar_batch_payload_schema(payload, cols, rows); uint32_t logical; if (!types || key >= *cols) return WL_COLUMNAR_SPILL_INVALID; if (wl_columnar_batch_payload_inline_schema(payload, &logical) || - types[key] != WIRELOG_TYPE_INT64) return WL_COLUMNAR_SPILL_UNSUPPORTED; + (types[key] != WIRELOG_TYPE_INT64 && + !(intern && + types[key] == + WIRELOG_TYPE_STRING))) return WL_COLUMNAR_SPILL_UNSUPPORTED; for (uint32_t c = 0; c < *cols; c++) - if (types[c] == - WIRELOG_TYPE_STRING) return WL_COLUMNAR_SPILL_UNSUPPORTED; + if (types[c] == WIRELOG_TYPE_STRING && + !wl_columnar_batch_payload_uses_intern(payload, intern)) + return WL_COLUMNAR_SPILL_UNSUPPORTED; return WL_COLUMNAR_SPILL_OK; } @@ -309,7 +313,8 @@ wl_columnar_payload_join_create_impl(wl_columnar_memory_governor_ref_t *g, uint32_t left_key, uint32_t right_key, uint64_t batch_bytes, wl_columnar_spill_cancel_fn cancel, void *cancel_context, wl_columnar_continuation_t **out, bool weighted, bool bounded, - uint64_t index_bytes, const uint32_t *projection, uint32_t project_count) + uint64_t index_bytes, const uint32_t *projection, uint32_t project_count, + const wl_intern_t *intern) { if (!out) return WL_COLUMNAR_SPILL_INVALID; *out = NULL; @@ -318,10 +323,14 @@ wl_columnar_payload_join_create_impl(wl_columnar_memory_governor_ref_t *g, g)) return WL_COLUMNAR_SPILL_INVALID; uint32_t lc, lr, rc, rr; wl_columnar_spill_status_t status = wl_columnar_payload_join_schema(left, - left_key, &lc, &lr); + left_key, &lc, &lr, intern); if (status != WL_COLUMNAR_SPILL_OK) return status; - status = wl_columnar_payload_join_schema(right, right_key, &rc, &rr); + status = wl_columnar_payload_join_schema(right, right_key, &rc, &rr, + intern); if (status != WL_COLUMNAR_SPILL_OK) return status; + if (wl_columnar_batch_payload_schema(left, NULL, NULL)[left_key] != + wl_columnar_batch_payload_schema(right, NULL, NULL)[right_key]) + return WL_COLUMNAR_SPILL_UNSUPPORTED; if (lc > UINT32_MAX - rc) return WL_COLUMNAR_SPILL_OVERFLOW; uint32_t input_cols = lc + rc; if (project_count && !projection) return WL_COLUMNAR_SPILL_INVALID; @@ -463,7 +472,7 @@ wl_columnar_payload_join_create(wl_columnar_memory_governor_ref_t *g, { return wl_columnar_payload_join_create_impl(g, left, right, left_key, right_key, batch_bytes, cancel, cancel_context, out, false, - false, 0, NULL, 0); + false, 0, NULL, 0, NULL); } wl_columnar_spill_status_t @@ -475,7 +484,7 @@ wl_columnar_payload_join_create_weighted(wl_columnar_memory_governor_ref_t *g, { return wl_columnar_payload_join_create_impl(g, left, right, left_key, right_key, batch_bytes, cancel, cancel_context, out, true, - false, 0, NULL, 0); + false, 0, NULL, 0, NULL); } wl_columnar_spill_status_t @@ -487,7 +496,7 @@ wl_columnar_payload_join_create_bounded(wl_columnar_memory_governor_ref_t *g, { return wl_columnar_payload_join_create_impl(g, left, right, left_key, right_key, batch_bytes, cancel, cancel_context, out, false, - true, index_bytes, NULL, 0); + true, index_bytes, NULL, 0, NULL); } wl_columnar_spill_status_t @@ -500,7 +509,7 @@ wl_columnar_payload_join_create_weighted_bounded( { return wl_columnar_payload_join_create_impl(g, left, right, left_key, right_key, batch_bytes, cancel, cancel_context, out, true, - true, index_bytes, NULL, 0); + true, index_bytes, NULL, 0, NULL); } wl_columnar_spill_status_t @@ -514,5 +523,19 @@ wl_columnar_payload_join_create_projected_bounded( { return wl_columnar_payload_join_create_impl(g, left, right, left_key, right_key, batch_bytes, cancel, cancel_context, out, false, - true, index_bytes, projection, project_count); + true, index_bytes, projection, project_count, NULL); +} + +wl_columnar_spill_status_t +wl_columnar_payload_join_create_reference_projected_bounded( + wl_columnar_memory_governor_ref_t *g, + wl_columnar_batch_payload_t *left, wl_columnar_batch_payload_t *right, + uint32_t left_key, uint32_t right_key, const uint32_t *projection, + uint32_t project_count, uint64_t batch_bytes, uint64_t index_bytes, + const wl_intern_t *intern, wl_columnar_spill_cancel_fn cancel, + void *cancel_context, wl_columnar_continuation_t **out) +{ + return wl_columnar_payload_join_create_impl(g, left, right, left_key, + right_key, batch_bytes, cancel, cancel_context, out, false, + true, index_bytes, projection, project_count, intern); } diff --git a/wirelog/columnar/payload_join.h b/wirelog/columnar/payload_join.h index 21ee48cf4..09c03de57 100644 --- a/wirelog/columnar/payload_join.h +++ b/wirelog/columnar/payload_join.h @@ -103,6 +103,22 @@ wl_columnar_spill_status_t wl_columnar_payload_join_create_projected_bounded( uint32_t project_count, uint64_t batch_bytes, uint64_t index_bytes, wl_columnar_spill_cancel_fn cancel, void *cancel_context, wl_columnar_continuation_t **out); +/* Reference-aware ordinary projected JOIN. Keys must both be INT64 or both + * STRING; every STRING-bearing input must retain the supplied originating + * intern table, even for non-key lanes. NULL intern is numeric-only. Legacy + * constructors remain numeric-only; INLINE/weighted/timed inputs are excluded. + * Output STRING IDs borrow the producer readers' retained capability while a + * batch is pending. A sink keeping independent payloads must capture with its + * own intern pin before acknowledgement (and before original-owner retirement). + * Raw copied IDs alone do not extend that lifetime. Existing caps/retry apply. */ +wl_columnar_spill_status_t +wl_columnar_payload_join_create_reference_projected_bounded( + wl_columnar_memory_governor_ref_t *governor, + wl_columnar_batch_payload_t *left, wl_columnar_batch_payload_t *right, + uint32_t left_key, uint32_t right_key, const uint32_t *projection, + uint32_t project_count, uint64_t batch_bytes, uint64_t index_bytes, + const wl_intern_t *intern, wl_columnar_spill_cancel_fn cancel, + void *cancel_context, wl_columnar_continuation_t **out); /* Only for a continuation returned by these constructors; NULL is INVALID. * Reports producer errors, not errors owned by the publication sink. */ wl_columnar_spill_status_t wl_columnar_payload_join_error( diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 19938a13a..73bb02f8d 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -692,7 +692,7 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, rp); bool selected_projection = projection && !strcmp(rp->ops[0].relation_name, name); - bool selected_join = !references && rp->op_count && + bool selected_join = rp->op_count && rp->ops[0].relation_name && !strcmp(rp->ops[0].relation_name, name) && wl_columnar_join_cold_prefix(sess, rp); @@ -714,7 +714,7 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, if (!op->right_relation || strcmp(op->right_relation, name) != 0) continue; - if (references || op->op != WL_PLAN_OP_JOIN || + if (op->op != WL_PLAN_OP_JOIN || op->key_count != 1 || op->right_filter_expr.size || (op->project_count && !op->project_indices)) return false; @@ -722,7 +722,8 @@ wl_columnar_session_cold_plan(const wl_col_session_t *sess, const char *name, names, op->right_keys ? op->right_keys[0] : NULL); /* The resident JOIN also falls back to column zero. */ if (key < 0) key = 0; - if (types && types[key] != WIRELOG_TYPE_INT64) return false; + if (types && types[key] != WIRELOG_TYPE_INT64 && + types[key] != WIRELOG_TYPE_STRING) return false; if (oi == 1 && rp->ops[0].op == WL_PLAN_OP_VARIABLE && !wl_columnar_join_cold_prefix(sess, rp)) return false; consumer = true; diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index d072d721d..b887ed8f8 100644 --- a/wirelog/wirelog-advanced.h +++ b/wirelog/wirelog-advanced.h @@ -219,7 +219,7 @@ wirelog_session_create_with_options(wirelog_program_t *program, * spill JOIN caps and a serial nonrecursive plan using only audited * VARIABLE/JOIN/MAP/CONSOLIDATE operations and the restricted FILTER below. * The selected name must have an eligible consumer: a JOIN input with - * one INT64 equality key or a complete + * one matching INT64 or STRING equality key or a complete * VARIABLE/MAP/CONSOLIDATE rule selecting only columns, optionally with one * FILTER before MAP comparing an INT64 column with an integer literal * (=, !=, <, >, <=, >=; column on the left). Projected results and consolidation @@ -228,10 +228,11 @@ wirelog_session_create_with_options(wirelog_program_t *program, * A cold left input is consumed by an initial VARIABLE/JOIN pair; both sides * may be sealed, including a self-JOIN. Subsequent JOINs consume an admitted * resident intermediate. No FILTER/MAP before that first cold JOIN, multi-key, - * cross/theta JOIN, non-INT64 key, right filter, timestamps, compound values, + * cross/theta JOIN, other key types, right filter, timestamps, compound values, * opaque/materialized rewrites, worker/control/delta modes or delta callback. - * Standalone direct/filtered projections also accept STRING lanes from the - * originating program intern table; reference-bearing JOINs remain unsupported. + * JOINs and standalone direct/filtered projections accept STRING lanes from + * the originating program intern table, including non-key payload columns. + * Both JOIN keys must have the same type; reference IDs are never remapped. * The program must outlive the session. Intern storage remains resident, and * sealing validates IDs even in unprojected STRING lanes. * Optimized plans may therefore be ineligible. The session must also not have From d0f3bc811a4df626ce97fe94e3cd8971c03dabcf Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 18:50:41 +0900 Subject: [PATCH 6/9] test(columnar): stabilize reference JOIN loop formatting --- tests/test_payload_join.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_payload_join.c b/tests/test_payload_join.c index fb07f3c76..2709cc77c 100644 --- a/tests/test_payload_join.c +++ b/tests/test_payload_join.c @@ -1397,9 +1397,8 @@ static void reference_joins(void) { const uint32_t map[] = {5, 1, 4, 2, 5}; const char *values[] = {"", "alpha", "한글"}; - for (unsigned string_key = 0; string_key < 2; - string_key++) for (unsigned mode = 0; - mode < 5; mode++) { + for (unsigned string_key = 0; string_key < 2; string_key++) { + for (unsigned mode = 0; mode < 5; mode++) { wl_columnar_memory_governor_ref_t *g = governor(1<<20); wl_intern_t *intern = wl_intern_create(); CHECK(intern && wl_intern_attach_memory_governor(intern, g) == 0); @@ -1529,6 +1528,7 @@ static void reference_joins(void) CHECK(wl_columnar_batch_payload_release(retained) == OK); CHECK(reserved(g) == 0); wl_columnar_memory_governor_ref_release(g); } + } } int main(void) From 5819841998bb691863f55c6dbe626ad101257e29 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 19:14:12 +0900 Subject: [PATCH 7/9] feat(columnar): initialize declared INLINE source schemas --- CHANGELOG.md | 5 ++ docs/MEMORY.md | 16 +++++ docs/SEMANTICS.md | 15 +++++ tests/test_join_batch_resume.c | 14 ++-- tests/test_plan_gen.c | 57 ++++++++++++++++ tests/test_plan_gen_oom.c | 15 ++++- tests/test_session_cold_relation.c | 103 +++++++++++++++++++++++++++++ wirelog/columnar/session.c | 75 +++++++++++++++++++++ wirelog/exec_plan.h | 10 +++ wirelog/exec_plan_gen.c | 40 +++++++++++ wirelog/ir/program.c | 21 ++++++ wirelog/ir/program.h | 3 + 12 files changed, 363 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b77e0c4e4..783a83eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to wirelog are documented in this file. ### Added +- **Declared INLINE source layout** (#1372): plans own logical declarations, + and ordinary sessions initialize governed flat INLINE metadata before facts + or raw inserts, including empty numeric inputs. Original unsupported body + patterns retain separate provenance for subsequent cold eligibility checks. + - **STRING cold JOIN consumers** (#1372): matching STRING or INT64 keys, STRING payloads, both cold directions, self JOIN and resident intermediate chains preserve originating intern identity and independently pinned output. diff --git a/docs/MEMORY.md b/docs/MEMORY.md index e1c88809d..1495cccd3 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -5116,3 +5116,19 @@ resumes a pending scalar retirement before switching owners, and enumerates prior/current/next owners exactly once. Public heads remain unchanged before cutoff; private row frontiers retire before successful publication. Maintenance funding only retires ownership and never constructs missing results. + +### Declaration-derived INLINE source layout (#1372) + +Parsed execution plans own name-less logical compound declarations alongside +packed physical lane types. Ordinary sessions initialize flat INLINE source +metadata before loading facts or accepting host rows, including empty numeric +inputs. Logical layout scratch and retained relation metadata are governed; +failed initialization releases partial source allocations. Field projections +produce scalar outputs even when their width equals the source physical width. + +Original nested/whole-compound body uses are recorded separately before lowering +loses that information. This summary restricts future cold eligibility, not +ordinary source initialization or existing resident evaluation. The declaration +record alone does not prove that a consumer supports a flat field view. SIDE +and mixed SIDE layouts retain their existing initialization behavior. This +preparation does not yet enable INLINE cold consumers. diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index d19ef1bfb..4d41a7605 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -1911,3 +1911,18 @@ refusals remain in force. Seed branches can execute again in subsequent rounds; this does not promise a universal raw performance improvement. Public heads and observer events retain the existing common cutoff. Raw epoch/rule-frontier arrays, weighted traces and incremental retraction are separate contracts. + +### INLINE declaration provenance for source initialization + +Execution plans retain owned logical declaration metadata independently of +physical child lane types. Declared flat INLINE inputs, including empty and +all-numeric inputs, receive their real logical arity map at session creation. +Host inserts still supply the declared physical scalar lanes. Extracted child +results remain scalar, rather than inheriting the original arity map merely +because physical widths coincide. + +A conservative per-relation summary preserves unsupported nested, whole-value, +wrong-functor and wrong-arity body uses before optimizer/lowering information +loss. It does not change existing resident semantics or prevent source schema +initialization. Missing declaration provenance cannot authorize future compound +cold reads; this preparation leaves INLINE sealing unsupported. diff --git a/tests/test_join_batch_resume.c b/tests/test_join_batch_resume.c index 51dc0f57e..3eb756ddb 100644 --- a/tests/test_join_batch_resume.c +++ b/tests/test_join_batch_resume.c @@ -7057,16 +7057,10 @@ test_owned_inline_relation(void) && source_rel->column_types && source_rel->column_types[1] == WIRELOG_TYPE_STRING); PAYLOAD_CHECK(seen && seen->nrows == 2); - /* The internal session does not install logical compound metadata. Bind - * the originating parsed declaration through the existing layout API. */ - col_rel_logical_col_t layout[3]; - for (uint32_t i = 0; i < 3; i++) { - layout[i].kind = schema->columns[i].compound_kind; - layout[i].arity = schema->columns[i].compound_arity; - layout[i].depth = layout[i].kind == - WIRELOG_COMPOUND_KIND_INLINE ? 1 : 0; - } - PAYLOAD_CHECK(col_rel_apply_compound_schema(source_rel, layout, 3) == 0); + PAYLOAD_CHECK(source_rel->compound_kind == WIRELOG_COMPOUND_KIND_INLINE && + source_rel->compound_count == 1 && + source_rel->compound_arity_len == 3 && + source_rel->compound_arity_map[1] == 2); wl_columnar_memory_governor_ref_t *g = cs->memory_governor; wl_columnar_memory_governor_ref_retain(g); wl_columnar_spill_store_t *store; diff --git a/tests/test_plan_gen.c b/tests/test_plan_gen.c index f1d47ceb3..be593a34b 100644 --- a/tests/test_plan_gen.c +++ b/tests/test_plan_gen.c @@ -1631,9 +1631,66 @@ test_load_facts_null_safe(void) * Main * ---------------------------------------------------------------- */ +static void +test_inline_declaration_provenance(void) +{ + TEST("owned INLINE declarations and original consumer provenance"); + const char *patterns[] = {"pair(s,n)", "pair(nested(s),n)", "whole", "_", + "wrong(s,n)", "pair(s)"}; + for (unsigned shape = 0; shape < 6; shape++) { + char text[1024]; + snprintf(text, sizeof(text), + ".decl a(id:int64,p:pair(symbol,int64) inline,m:int64,q:box/1 inline)\n" + ".decl out(id:int64)\n.decl safe(id:int64)\n" + "out(i) :- a(i,%s,m,box(z)).\n" + "safe(i) :- a(i,pair(s,n),m,box(z)).\n", patterns[shape]); + wirelog_error_t err; + wirelog_program_t *prog = wirelog_parse_string(text, &err); + ASSERT(prog, "INLINE provenance fixture must parse"); + const wirelog_schema_t *decl = wirelog_program_get_schema(prog, "a"); + ASSERT(decl && decl->column_count == 4, "logical declaration width"); + wl_plan_t *plan = NULL; + ASSERT(wl_plan_from_program(prog, &plan) == 0 && plan, + "plan generation"); + uint32_t edb = 0; + while (edb < plan->edb_count && strcmp(plan->edb_relations[edb], + "a")) edb++; + ASSERT(edb < plan->edb_count && plan->edb_schemas, + "owned EDB declaration"); + const wl_plan_edb_schema_t *schema = &plan->edb_schemas[edb]; + ASSERT(schema->column_count == 4 && schema->columns != decl->columns, + "logical schema copy"); + ASSERT(schema->has_unsupported_inline_cold_use == (shape != 0), + "unsupported summary survives later safe use"); + ASSERT(plan->edb_column_type_counts[edb] == 5 && + plan->edb_column_types[edb][1] == WIRELOG_TYPE_STRING && + plan->edb_column_types[edb][2] == WIRELOG_TYPE_INT64 && + plan->edb_column_types[edb][4] == WIRELOG_TYPE_INT64, + "packed physical types use logical stride"); + for (unsigned i = 0; i < 4; i++) { + ASSERT(schema->columns[i].name == NULL, "name-less copy"); + ASSERT(schema->columns[i].compound_kind == + decl->columns[i].compound_kind && + schema->columns[i].compound_arity == + decl->columns[i].compound_arity && + schema->columns[i].compound_functor_id == + decl->columns[i].compound_functor_id && + schema->columns[i].compound_inline_col_offset == + decl->columns[i].compound_inline_col_offset, + "semantic declaration metadata preserved"); + } + ASSERT(schema->columns[1].compound_inline_col_offset == 1 && + schema->columns[3].compound_inline_col_offset == 4, + "multiple groups with intervening scalar"); + wl_plan_free(plan); wirelog_program_free(prog); + } + PASS(); +} + int main(void) { + test_inline_declaration_provenance(); printf("=== Plan Generator Tests ===\n"); test_delta_name_invariant(); diff --git a/tests/test_plan_gen_oom.c b/tests/test_plan_gen_oom.c index 610af78c1..cedacfe9f 100644 --- a/tests/test_plan_gen_oom.c +++ b/tests/test_plan_gen_oom.c @@ -49,11 +49,14 @@ main(void) ".decl b(x: int64, value: int64)\n" ".decl c(x: int64, value: int64)\n" ".decl guard(q: int64)\n" + ".decl inline_a(id:int64,p:pair(symbol,int64) inline)\n" + ".decl inline_b(p:box/1 inline,x:int64,q:pair/2 inline)\n" ".decl out(x: int64, av: int64, bv: int64, cv: int64)\n" "out(x, av, bv, cv) :- a(x, av), b(x, bv), c(x, cv), guard(7).\n"; bool saw_success = false; bool saw_injected_failure = false; bool saw_lftj = false; + bool saw_inline = false; for (long attempt = 0; attempt < 512; attempt++) { wirelog_error_t error; @@ -93,6 +96,15 @@ main(void) } } } + unsigned declarations = 0; + for (uint32_t i = 0; i < plan->edb_count; i++) + declarations += plan->edb_schemas && + plan->edb_schemas[i].columns != NULL; + if (declarations != 2) { + fprintf(stderr, "successful plan lost compound declarations\n"); + wl_plan_free(plan); wirelog_program_free(program); return 1; + } + saw_inline = true; saw_success = true; wl_plan_free(plan); } else if (plan) { @@ -106,5 +118,6 @@ main(void) wirelog_program_free(program); } - return saw_success && saw_injected_failure && saw_lftj ? 0 : 1; + return saw_success && saw_injected_failure && saw_lftj && + saw_inline ? 0 : 1; } diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 3af80450e..73565f725 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -2502,6 +2502,107 @@ static void reference_join_bounded_scratch(void) CHECK(peaks[0] == peaks[1]); } +static void inline_initialized_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + unsigned *bits = context; + unsigned which = !strcmp(name, "out") ? 0 : !strcmp(name, + "filtered") ? 1 : 2; + CHECK(which != 2 || !strcmp(name, "joined")); + CHECK(width == 3 && row[0] == 7 && row[1] == 11 && row[2] == 13); + CHECK(!(*bits & (1u< 0.\n" + "joined(i,x,y+0) :- a(i,pair(x,y)),b(x,z).\n"; + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + col_rel_t *a = session_find_rel(s, "a"); + CHECK(a && !a->nrows && a->ncols == 3 && a->column_types && + a->compound_kind == WIRELOG_COMPOUND_KIND_INLINE && + a->compound_count == 1 && a->compound_arity_len == 2 && + a->compound_arity_map[0] == 1 && a->compound_arity_map[1] == 2); + int64_t row[] = {7, 11, 13}, b[] = {11, 17}; + CHECK(wl_session_insert(f.session, "a", row, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + for (unsigned repeat = 0; repeat < 2; repeat++) { + unsigned bits = 0; + CHECK(wl_session_snapshot(f.session, inline_initialized_observe, + &bits) == 0 && bits == 7); + const char *names[] = {"out", "filtered", "joined"}; + for (unsigned i = 0; i < 3; i++) { + col_rel_t *out = session_find_rel(s, names[i]); + CHECK(out && out->ncols == 3 && + out->compound_kind == WIRELOG_COMPOUND_KIND_NONE && + !out->compound_count && !out->compound_arity_map); + } + CHECK(wl_session_step(f.session) == 0); + } + destroy(&f); +} + +static void inline_initialization_budget(void) +{ + const char *text = ".decl a(i:int64,p:pair/2 inline)\n" + ".decl out(i:int64)\nout(i) :- a(i,pair(x,y)).\n"; + uint64_t floor = 0; + for (unsigned pass = 0; pass < 3; pass++) { + bool fits = pass != 1; + fixture_t f = {0}; wirelog_error_t error; + f.program = wirelog_parse_string(text, &error); CHECK(f.program); + CHECK(wl_plan_from_program(f.program, &f.plan) == 0); + uint64_t limit = pass == 0 ? 4*1024*1024 : floor - (fits ? 0 : 1); + wl_columnar_memory_resolution_t resolution = { + .budget_bytes = limit, .usable_bytes = limit, + .mode = WL_COLUMNAR_MEMORY_MODE_ENFORCING, + .source = WL_COLUMNAR_MEMORY_SOURCE_ENV, + .status = WL_COLUMNAR_MEMORY_OK + }; + f.governor = wl_columnar_memory_governor_ref_create(&resolution); + CHECK(f.governor); + wl_session_options_t options; wl_session_options_init(&options); + options.memory_governor = f.governor; + options.spill_directory = test_tmpdir(); + options.spill_quota_bytes = 1024*1024; options.spill_max_row_bytes = 64; + options.spill_join_batch_bytes = 96; + options.spill_join_index_bytes = + sizeof(wl_columnar_memory_reservation_t)+16; + budget_peak = 0; + wl_columnar_memory_governor_test_admitted = budget_admitted; + int rc = wl_session_create_with_options(wl_backend_columnar(), f.plan, + 1, &options, &f.session); + wl_columnar_memory_governor_test_admitted = NULL; + if (!pass) { + floor = budget_peak; CHECK(floor > 1); + } + if (fits) { + CHECK(rc == 0 && f.session); + col_rel_t *a = session_find_rel(COL_SESSION(f.session), "a"); + CHECK(a && a->compound_arity_len == 2 && a->ncols == 3); + } else CHECK(rc == WL_ERR_MEMORY_BUDGET && !f.session); + destroy(&f); + } + const char *patterns[] = {"pair(nested(s),n)", "whole", "_"}; + for (unsigned i = 0; i < 3; i++) { + char query_text[256]; + snprintf(query_text, sizeof(query_text), + ".decl a(i:int64,p:pair/2 inline)\n" + ".decl out(i:int64)\nout(i) :- a(i,%s).\n", patterns[i]); + fixture_t f = fixture(query_text, 4*1024*1024, 1024*1024); + col_rel_t *a = session_find_rel(COL_SESSION(f.session), "a"); + CHECK(a && a->compound_kind == WIRELOG_COMPOUND_KIND_INLINE && + a->ncols == 3); + destroy(&f); + } +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -2511,6 +2612,8 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + inline_initialization_budget(); + inline_initialized_sources(); reference_join_reclamation(); reference_join_bounded_scratch(); reference_join_directions(); reference_projection_memory(); diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 73bb02f8d..80af5037d 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -3974,6 +3974,74 @@ col_compute_worker_cap(uint64_t ram_bytes) * @see wl_session_create in session.c for vtable dispatch context * @see wl_col_session_t memory layout documentation above */ +/* Initialize genuine flat INLINE layout before fact loading/host insertion. + * Consumer eligibility is separate: unsafe body uses do not alter storage. */ +static int +wl_columnar_session_inline_schema(col_rel_t *r, const wl_plan_t *plan, + uint32_t edb) +{ + const wl_plan_edb_schema_t *schema = + plan->edb_schemas ? &plan->edb_schemas[edb] : NULL; + if (!schema || !schema->columns) return 0; + bool has_inline = false; + for (uint32_t i = 0; i < schema->column_count; i++) { + if (schema->columns[i].compound_kind == + WIRELOG_COMPOUND_KIND_SIDE) return 0; + has_inline |= schema->columns[i].compound_kind == + WIRELOG_COMPOUND_KIND_INLINE; + } + if (!has_inline) return 0; + if (!plan->edb_column_types || !plan->edb_column_type_counts || + !plan->edb_column_types[edb] || !plan->intern) return EINVAL; + uint64_t physical = 0; + for (uint32_t i = 0; i < schema->column_count; i++) { + const wirelog_column_t *c = &schema->columns[i]; + if (c->compound_kind == WIRELOG_COMPOUND_KIND_INLINE) { + if (!c->compound_arity || + c->compound_arity > WL_COMPOUND_INLINE_MAX_ARITY || + c->compound_inline_col_offset != physical || + !wl_intern_reverse(plan->intern, + c->compound_functor_id)) return EINVAL; + physical += c->compound_arity; + } else if (c->compound_kind == WIRELOG_COMPOUND_KIND_NONE) physical++; + else return EINVAL; + } + if (physical != plan->edb_column_type_counts[edb]) return EINVAL; + uint64_t bytes; + if (!wl_columnar_memory_size_mul(schema->column_count, + sizeof(col_rel_logical_col_t), + &bytes) || bytes > SIZE_MAX) return EOVERFLOW; + wl_columnar_memory_reservation_t reservation; + wl_columnar_memory_reservation_init(&reservation); + wl_columnar_memory_admission_status_t admitted = + wl_columnar_memory_reserve_checked( + wl_columnar_memory_governor_ref_get(r->memory_governor), bytes, + &reservation); + if (admitted > WL_COLUMNAR_MEMORY_ADMISSION_ADVISORY) return admitted == + WL_COLUMNAR_MEMORY_ADMISSION_DENIED ? ENOSPC : EOVERFLOW; + col_rel_logical_col_t *layout = calloc(schema->column_count, + sizeof(*layout)); + if (!layout) { + if (!wl_columnar_memory_rollback(&reservation)) abort(); return ENOMEM; + } + if (!wl_columnar_memory_commit(&reservation, layout)) abort(); + for (uint32_t i = 0; i < schema->column_count; i++) { + layout[i].kind = schema->columns[i].compound_kind; + layout[i].arity = schema->columns[i].compound_arity; + layout[i].depth = layout[i].kind == + WIRELOG_COMPOUND_KIND_INLINE ? 1 : 0; + } + int rc = 0; + if (!r->ncols) rc = col_rel_set_schema(r, (uint32_t)physical, NULL); + if (!rc) rc = col_rel_set_column_types(r, plan->edb_column_types[edb], + (uint32_t)physical); + if (!rc) rc = col_rel_apply_compound_schema(r, layout, + schema->column_count); + free(layout); + if (!wl_columnar_memory_release(&reservation)) abort(); + return rc; +} + static int col_session_create_internal(const wl_plan_t *plan, uint32_t num_workers, const wl_session_options_t *options, wl_session_t **out) @@ -4454,6 +4522,13 @@ col_session_create_internal(const wl_plan_t *plan, uint32_t num_workers, goto oom; } } + rc = wl_columnar_session_inline_schema(r, plan, i); + if (rc != 0) { + relation_create_rc = rc == ENOMEM && + r->memory_budget_denial_pending ? ENOSPC : rc; + col_rel_destroy(r); + goto oom; + } rc = session_add_rel(sess, r); if (rc != 0) { relation_create_rc = rc == ENOMEM diff --git a/wirelog/exec_plan.h b/wirelog/exec_plan.h index 36ce737a8..9894f04d2 100644 --- a/wirelog/exec_plan.h +++ b/wirelog/exec_plan.h @@ -727,6 +727,15 @@ typedef struct { * zero-arity declaration such as `.decl p()`. */ #define WL_PLAN_WIDTH_UNDECLARED UINT32_MAX +/* Optional declaration provenance for each EDB. Columns are owned name-less + * copies; physical child types remain in edb_column_types. Missing metadata + * never grants compound cold eligibility to a hand-built legacy plan. */ +typedef struct { + const wirelog_column_t *columns; + uint32_t column_count; + bool has_unsupported_inline_cold_use; +} wl_plan_edb_schema_t; + typedef struct { const wl_plan_stratum_t *strata; uint32_t stratum_count; @@ -747,6 +756,7 @@ typedef struct { * plans' int64 default. */ const wirelog_column_type_t *const *edb_column_types; const uint32_t *edb_column_type_counts; + const wl_plan_edb_schema_t *edb_schemas; struct wl_intern *intern; /* borrowed from program; lifetime >= plan */ } wl_plan_t; diff --git a/wirelog/exec_plan_gen.c b/wirelog/exec_plan_gen.c index 49aae581f..1b8c6830f 100644 --- a/wirelog/exec_plan_gen.c +++ b/wirelog/exec_plan_gen.c @@ -4349,6 +4349,11 @@ wl_plan_free(wl_plan_t *plan) free((void *)plan->edb_column_types); } free((void *)plan->edb_column_type_counts); + if (plan->edb_schemas) { + for (uint32_t i = 0; i < plan->edb_count; i++) + free((void *)plan->edb_schemas[i].columns); + free((void *)plan->edb_schemas); + } free(plan); } @@ -4653,6 +4658,41 @@ wl_plan_from_program_with_snapshot(struct wirelog_program *prog, = (const wirelog_column_type_t *const *)edb_types; plan->edb_column_type_counts = (const uint32_t *)edb_type_counts; + /* Install ownership before any nested allocation can fail. */ + wl_plan_edb_schema_t *schemas = calloc(edb_count ? edb_count : 1, + sizeof(*schemas)); + if (!schemas) { + wl_plan_free(plan); return -1; + } + plan->edb_schemas = schemas; + for (uint32_t i = 0; i < edb_count; i++) { + const wl_ir_relation_info_t *rel = NULL; + for (uint32_t j = 0; j < prog->relation_count; j++) { + if (prog->relations[j].name && !strcmp(prog->relations[j].name, + edb_rels[i])) { + rel = &prog->relations[j]; break; + } + } + if (!rel || !rel->has_decl || !rel->column_count) continue; + bool compound = false; + for (uint32_t c = 0; c < rel->column_count; c++) + compound |= rel->columns[c].compound_kind != + WIRELOG_COMPOUND_KIND_NONE; + if (!compound) continue; + wirelog_column_t *columns = calloc(rel->column_count, sizeof(*columns)); + if (!columns) { + wl_plan_free(plan); return -1; + } + schemas[i].columns = columns; + schemas[i].column_count = rel->column_count; + schemas[i].has_unsupported_inline_cold_use = + rel->has_unsupported_inline_cold_use; + for (uint32_t c = 0; c < rel->column_count; c++) { + columns[c] = rel->columns[c]; + columns[c].name = NULL; + } + } + /* ---------------------------------------------------------------- * Build strata * ---------------------------------------------------------------- */ diff --git a/wirelog/ir/program.c b/wirelog/ir/program.c index 3ba8e8e07..df6aa72aa 100644 --- a/wirelog/ir/program.c +++ b/wirelog/ir/program.c @@ -2177,6 +2177,27 @@ build_atom_scan(const wl_parser_ast_node_t *atom, uint32_t arg_count = atom->child_count; wl_ir_relation_info_t *rel_info = find_relation_info(prog, atom->name); + /* Preserve every original use before optimizer cloning/removal and before + * physical lowering turns unsupported nested children into anonymous lanes. */ + for (uint32_t i = 0; rel_info && i < rel_info->column_count; i++) { + const wirelog_column_t *col = &rel_info->columns[i]; + if (col->compound_kind != WIRELOG_COMPOUND_KIND_INLINE) continue; + const wl_parser_ast_node_t *arg = i < + atom->child_count ? atom->children[i] : NULL; + if (!compound_arg_functor_matches(arg, col, prog)) { + rel_info->has_unsupported_inline_cold_use = true; + continue; + } + for (uint32_t c = 0; c < arg->child_count; c++) { + wl_parser_ast_node_type_t type = arg->children[c]->type; + if (type != WL_PARSER_AST_NODE_VARIABLE && + type != WL_PARSER_AST_NODE_WILDCARD && + type != WL_PARSER_AST_NODE_INTEGER && + type != WL_PARSER_AST_NODE_FLOAT && + type != WL_PARSER_AST_NODE_STRING) + rel_info->has_unsupported_inline_cold_use = true; + } + } uint32_t physical_count = atom_physical_column_count(atom, rel_info, prog); scan->column_names diff --git a/wirelog/ir/program.h b/wirelog/ir/program.h index d142a70a5..8011e3bc7 100644 --- a/wirelog/ir/program.h +++ b/wirelog/ir/program.h @@ -74,6 +74,9 @@ typedef struct { uint32_t fact_capacity; bool has_float_facts; /* prevents legacy executor reinterpretation */ bool has_float_compound_slots; /* includes side compounds */ + /* Accumulated before lowering loses nested/whole INLINE body patterns. + * Restricts cold scalar-field consumption only, never ordinary execution. */ + bool has_unsupported_inline_cold_use; /* Issue #535: RDF named-graph support */ bool has_graph_column; uint32_t graph_column_index; /* valid only when has_graph_column == true */ From 0e305bc42a47912c53480bb61b17f8515499122a Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 19:38:32 +0900 Subject: [PATCH 8/9] feat(columnar): consume sealed INLINE fields --- CHANGELOG.md | 8 + docs/MEMORY.md | 54 +++- docs/SEMANTICS.md | 11 +- tests/test_payload_join.c | 40 ++- tests/test_session_cold_relation.c | 456 ++++++++++++++++++++++++++++ tests/test_wirelog_advanced_spill.c | 103 +++++++ wirelog/columnar/internal.h | 10 +- wirelog/columnar/join.c | 27 +- wirelog/columnar/payload_join.c | 10 +- wirelog/columnar/payload_join.h | 5 +- wirelog/columnar/session.c | 99 +++++- wirelog/wirelog-advanced.h | 10 +- 12 files changed, 788 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 783a83eea..a3e899822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to wirelog are documented in this file. ### Added +- **Flat INLINE cold field consumers** (#1372): bounded projection/JOIN paths + consume declared scalar children, including STRING keys and payloads, through + either cold input, self JOINs and resident intermediate chains. Source layout + and original-use provenance are checked before retirement; copied semantic + metadata and intern pins preserve functors even for numeric-only INLINE. + Public typed/failure/memory tests retain explicit resident metadata/output + floors. Whole compounds, SIDE and nested handles remain unsupported. + - **Declared INLINE source layout** (#1372): plans own logical declarations, and ordinary sessions initialize governed flat INLINE metadata before facts or raw inserts, including empty numeric inputs. Original unsupported body diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 1495cccd3..53e05198c 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -4726,10 +4726,10 @@ producer/output errors are covered by `test_batch_payload` and `test_join_spill` This completes the owned intermediate subset; it does not make registry or persistent trace storage cold, nor add differential cold execution. -### Persistent sealed numeric JOIN input (#1372) +### Persistent sealed projection and JOIN inputs (#1372) The internal `wl_columnar_session_seal_input` operation converts one eligible -numeric EDB into a session-owned immutable cold binding before the +scalar or declared flat INLINE EDB into a session-owned immutable cold binding before the first STEP or SNAPSHOT attempt. It captures privately, checks exclusive resident retirement, refunds the original allocation, removes the raw registry entry, and publishes the binding without a fallible gap. Failed capture, cancellation, @@ -4747,9 +4747,9 @@ earlier bindings and their disk extents, and preserve the failed resident source Duplicate sealing is refused. Destruction releases every node and payload before the session store. Raw lookup of any known cold name remains an explicit error. -Complete direct/narrowly filtered projection rules over another loaded numeric +Complete direct/narrowly filtered projection rules over another eligible loaded EDB can coexist whether that source is resident or already sealed. Auditing uses -that source's complete numeric schema and metadata, not the selected candidate's +that source's complete physical schema and declaration provenance, not the selected candidate's width/types. This does not admit arbitrary resident filters or filters on IDBs, and another source's valid projection cannot authorize an unused candidate. @@ -4757,13 +4757,14 @@ The supported plan is serial, nonrecursive, and contains only audited VARIABLE/JOIN/MAP/CONSOLIDATE operations, plus the restricted projection FILTER described below. The cold name must have an eligible consumer: a JOIN input or the exact direct/filtered projection body below; it cannot be a relation head -or an arbitrary VARIABLE consumer. Eligible JOINs use one INT64 key, -scalar numeric rows, no timestamps and no right filter. Multiple eligible JOINs +or an arbitrary VARIABLE consumer. Eligible JOINs use one matching INT64 or +STRING key, supported scalar lanes (including declared flat INLINE fields), +no timestamps and no right filter. Multiple eligible JOINs may consume the same binding. A cold left source requires an initial VARIABLE/JOIN pair. Both inputs can be sealed, and a self-JOIN acquires two independent reader pins over one persistent owner. A subsequent JOIN consumes an admitted resident intermediate. FILTER/MAP before the first cold JOIN, -multi-key/cross/theta JOINs and non-INT64 keys are refused. Available source +multi-key/cross/theta JOINs and other key types are refused. Available source schemas and projection bounds are checked before retirement and again at execution; an unloaded resident counterpart is checked when its schema exists. Spill batch/index caps must be positive. Opaque @@ -4851,7 +4852,7 @@ retained-source control remains the causal reclamation proof; this public test qualifies the actual host boundary and output floor. After loading all inputs, call `wirelog_session_seal_input` for one eligible -numeric input. `wirelog_session_seal_join_input` remains a compatibility spelling +scalar or declared flat INLINE input. `wirelog_session_seal_join_input` remains a compatibility spelling with identical eligibility, errors and lifetime. A projection-only program can seal its source without a JOIN. It uses the exact immutable plan/mode restrictions above; opaque optimized plans, VARIABLE reads outside the direct-projection subset, recursive, worker, @@ -4929,7 +4930,7 @@ actual corresponding CI run, not a Linux macro compile. ### Bounded direct projections of sealed JOIN inputs (#1372) -A sealed numeric JOIN input can also supply an exact three-operator +An eligible sealed JOIN input can also supply an exact three-operator `VARIABLE(cold) → MAP(columns) → CONSOLIDATE` rule. For example, alongside `result(k,x,y + 0) :- a(k,x), b(k,y).`, a declared `keys(k)` head can use `keys(k) :- b(k,v).`. Such a projection may also be the source's only consumer: @@ -5014,7 +5015,8 @@ controls cover one and both input owners without hiding resident output costs. ### Remaining #1372 implementation groups -The sealed scalar numeric/STRING JOIN and projection subsets now have actual persistent cold storage, +The sealed scalar numeric/STRING and declared flat INLINE field JOIN/projection +subsets now have actual persistent cold storage, resident reclamation and evaluator consumers. General cold VARIABLE/direct-column access beyond the exact direct-projection subset, mutable relations, persistent trace/differential storage and their arrangement consumers remain separate integration work. They require stable @@ -5130,5 +5132,33 @@ Original nested/whole-compound body uses are recorded separately before lowering loses that information. This summary restricts future cold eligibility, not ordinary source initialization or existing resident evaluation. The declaration record alone does not prove that a consumer supports a flat field view. SIDE -and mixed SIDE layouts retain their existing initialization behavior. This -preparation does not yet enable INLINE cold consumers. +and mixed SIDE layouts retain their existing initialization behavior. Flat INLINE field consumers use this provenance as described below. + +### Flat INLINE cold field consumers (#1372, partial) + +The same bounded projection and JOIN paths now accept real declaration-derived +flat INLINE sources. Preflight compares logical kind/arity/offset/functor and +all physical lane types with the initialized source. The source must have an +owned plan declaration and no original unsupported body use. Nested/whole-value, +wrong-functor/arity and SIDE consumers remain ineligible even when lowering +would otherwise resemble a scalar projection. Missing declarations, invalid +functors, mismatched layouts and invalid STRING IDs refuse before retirement. +Runtime rechecks every binding against its originating declaration capability. + +Capture copies semantic declarations into the payload and pins the originating +intern table, including numeric-only INLINE sources whose functor is the only +reference. Reordered/extracted output lanes keep their scalar types and do not +inherit the source arity map. STRING child keys use the same intern-identity +rule as scalar STRING JOINs. Both input directions, both-cold/self JOINs, +resident intermediate chains and independent scans share this contract. + +Tests use parsed STRING-child declarations and automatically initialized source +metadata, with empty/Unicode values, physical raw inserts, multiple logical +INLINE groups and actual typed public output. Malformed layout/provenance and +allocation/write/read/cancellation failures preserve source/prior bindings and +refund private work. Numeric-only INLINE explicitly retains functor capability. +Fixed-intern/schema two-cardinality checks retain bounded JOIN scratch; paired +retained-source controls prove projection and one/two-owner JOIN reclamation. +Intern/declaration storage, resident output and intermediate floors remain. +The program must still outlive public sessions; no public lifetime shortcut, +whole compound reconstruction, SIDE or nested-handle ownership is introduced. diff --git a/docs/SEMANTICS.md b/docs/SEMANTICS.md index 4d41a7605..a70d2cec4 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -310,7 +310,9 @@ Every STRING lane is validated at capture, including unprojected lanes. Empty strings and non-ASCII strings retain their ordinary intern identity. JOINs also preserve STRING payload lanes and compare STRING keys only after both inputs prove the same originating intern table. Coincident IDs from another -table are refused; compound sources remain unsupported. Pins retain backing storage; the +table are refused; SIDE/nested layouts and whole-compound consumers remain +unsupported. Declared flat INLINE fields may use the same scalar projection +and JOIN shapes after source-layout and original-use provenance checks. Pins retain backing storage; the public program must still outlive its sessions, and intern storage is a resident memory floor. A copied raw ID alone does not extend that lifetime. @@ -1925,4 +1927,9 @@ A conservative per-relation summary preserves unsupported nested, whole-value, wrong-functor and wrong-arity body uses before optimizer/lowering information loss. It does not change existing resident semantics or prevent source schema initialization. Missing declaration provenance cannot authorize future compound -cold reads; this preparation leaves INLINE sealing unsupported. +cold reads. Supported scalar field projections and JOINs can seal real flat +INLINE sources only after validating their initialized layout against these +declarations and the original-use summary. Numeric-only INLINE still pins the +functor intern table. Output lanes retain scalar types without the source arity +map; wrong functor/arity, nested and whole-compound uses refuse before retiring +the source. diff --git a/tests/test_payload_join.c b/tests/test_payload_join.c index 2709cc77c..9122768a7 100644 --- a/tests/test_payload_join.c +++ b/tests/test_payload_join.c @@ -1360,7 +1360,7 @@ static void projected(void) } static col_rel_t *reference_source(wl_intern_t *intern, unsigned rows, - bool string_key) + bool string_key, bool inlined) { const char *values[] = {"", "alpha", "한글"}; int64_t ids[3]; @@ -1376,6 +1376,12 @@ static col_rel_t *reference_source(wl_intern_t *intern, unsigned rows, int64_t row[] = {string_key?ids[i%3]:(int64_t)(i%3), i, ids[(i+1)%3]}; CHECK(col_rel_append_row(r, row) == 0); } + if (inlined) { + CHECK(wl_intern_put(intern, "pair") >= 0); + col_rel_logical_col_t layout[] = {{WIRELOG_COMPOUND_KIND_NONE, 0, 0}, + {WIRELOG_COMPOUND_KIND_INLINE, 2, 1}}; + CHECK(col_rel_apply_compound_schema(r, layout, 2) == 0); + } return r; } static wl_columnar_batch_payload_t *reference_capture( @@ -1384,16 +1390,27 @@ static wl_columnar_batch_payload_t *reference_capture( unsigned mode) { wl_columnar_batch_payload_t *p = NULL; - CHECK(wl_columnar_batch_payload_capture_with_intern(g, store, r, r->nrows, - mode == + wirelog_column_t columns[2] = {0}; + columns[0].type = r->column_types[0]; + columns[1].type = WIRELOG_TYPE_INT32; + columns[1].compound_kind = WIRELOG_COMPOUND_KIND_INLINE; + columns[1].compound_functor_id = (uint32_t)wl_intern_get(intern, "pair"); + columns[1].compound_arity = 2; columns[1].compound_inline_col_offset = 1; + wl_columnar_batch_payload_storage_t storage = mode == 1 ? WL_COLUMNAR_BATCH_PAYLOAD_SPILLED : - WL_COLUMNAR_BATCH_PAYLOAD_RESIDENT, - intern, NULL, NULL, &p) == OK); + WL_COLUMNAR_BATCH_PAYLOAD_RESIDENT; + CHECK((r->compound_kind == WIRELOG_COMPOUND_KIND_INLINE ? + wl_columnar_batch_payload_capture_with_inline(g, store, r, r->nrows, + storage, + columns, 2, intern, NULL, NULL, &p) : + wl_columnar_batch_payload_capture_with_intern(g, store, r, r->nrows, + storage, + intern, NULL, NULL, &p)) == OK); if (mode == 2)CHECK(wl_columnar_batch_payload_spill(&p, store, NULL, NULL) == OK); return p; } -static void reference_joins(void) +static void reference_joins(bool inlined) { const uint32_t map[] = {5, 1, 4, 2, 5}; const char *values[] = {"", "alpha", "한글"}; @@ -1405,8 +1422,8 @@ static void reference_joins(void) wl_columnar_spill_store_t *store; CHECK(wl_columnar_spill_store_open(test_tmpdir(), g, 65536, 64, &store) == OK); - col_rel_t *l = reference_source(intern, 9, string_key), - *r = reference_source(intern, 11, string_key); + col_rel_t *l = reference_source(intern, 9, string_key, inlined), + *r = reference_source(intern, 11, string_key, inlined); wl_columnar_batch_payload_t *lp = reference_capture(g, store, l, intern, mode == 4?2:mode%2); wl_columnar_batch_payload_t *rp = reference_capture(g, store, r, @@ -1427,7 +1444,7 @@ static void reference_joins(void) wl_intern_t *foreign = wl_intern_create(); CHECK(foreign && wl_intern_attach_memory_governor(foreign, g) == 0); CHECK(wl_intern_put(foreign, "different-at-zero") == 0); - col_rel_t *f = reference_source(foreign, 3, string_key); + col_rel_t *f = reference_source(foreign, 3, string_key, inlined); wl_columnar_batch_payload_t *fp = reference_capture(g, store, f, foreign, 1); col_rel_destroy(f); CHECK(wl_columnar_payload_join_create_reference_projected_bounded(g, @@ -1435,7 +1452,7 @@ static void reference_joins(void) &join) == WL_COLUMNAR_SPILL_UNSUPPORTED && !join); CHECK(wl_columnar_batch_payload_release(fp) == OK); wl_intern_free(foreign); - f = reference_source(intern, 3, !string_key); + f = reference_source(intern, 3, !string_key, inlined); fp = reference_capture(g, store, f, intern, 0); col_rel_destroy(f); CHECK(wl_columnar_payload_join_create_reference_projected_bounded(g, lp, fp, 0, 0, map, 5, 40, index_cap(1), intern, NULL, NULL, @@ -1491,6 +1508,7 @@ static void reference_joins(void) types[3] == WIRELOG_TYPE_STRING && types[4] == WIRELOG_TYPE_STRING); CHECK(wl_columnar_batch_payload_uses_intern(out, intern)); + CHECK(!wl_columnar_batch_payload_inline_schema(out, NULL)); wl_columnar_batch_payload_reader_t *reader; CHECK(wl_columnar_batch_payload_acquire(out, &reader) == OK); @@ -1533,7 +1551,7 @@ static void reference_joins(void) int main(void) { - reference_joins(); + reference_joins(false); reference_joins(true); projected(); combinations(); failures(); budget(false); budget(true); misses(); concurrent_self_join(false); concurrent_self_join(true); diff --git a/tests/test_session_cold_relation.c b/tests/test_session_cold_relation.c index 73565f725..c4f404789 100644 --- a/tests/test_session_cold_relation.c +++ b/tests/test_session_cold_relation.c @@ -2603,6 +2603,458 @@ static void inline_initialization_budget(void) } } +static void inline_join_directions(void) +{ + for (unsigned numeric = 0; numeric < 2; numeric++) + for (unsigned shape = 0; shape < 5; shape++) + for (unsigned direction = 0; direction < 5; direction++) { + char text[512]; + snprintf(text, sizeof(text), + ".decl a(p:triple(%s,int64,symbol) inline)\n" + ".decl b(p:triple(%s,int64,symbol) inline)\n" + ".decl result(k:%s,x:int64,p:symbol,y:int64,q:symbol)\n" + "result(k,x,p,y+0,q) :- a(triple(k,x,p)),b(triple(k,y,q)).\n", + numeric ? "int64" : "symbol", numeric ? "int64" : "symbol", + numeric ? "int64" : "symbol"); + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + int64_t ids[3]; + for (unsigned i = 0; i < 3; i++) + ids[i] = wl_intern_put(s->intern, reference_values[i]); + int64_t absent = numeric ? 99 : wl_intern_put(s->intern, + "absent"); + wl_plan_relation_t *rp = join_body(&f, "result"); + CHECK(rp->ops[0].op == WL_PLAN_OP_VARIABLE && + !strcmp(rp->ops[0].relation_name, "a") && + rp->ops[1].op == WL_PLAN_OP_JOIN); + for (unsigned i = 0; i < 12; i++) { + int64_t a[] = {numeric ? (int64_t)(i%3) : ids[i%3], i, + ids[(i+1)%3]}; + int64_t b[] = {shape == 1 ? absent : a[0], i, ids[(i+2)%3]}; + if (shape != 2 && shape != 4) + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + if (shape != 3 && shape != 4) + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + } + seal_direction(&f, direction); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + if (direction && !shape) { + unsigned count = 0; read_fault = 7; + CHECK(wl_session_snapshot(f.session, count_only, + &count) == EIO && !count); + read_fault = 0; + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == + disk); + } + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_join_events_t e = {.session = s, + .numeric = numeric != 0}; + CHECK(wl_session_snapshot(f.session, reference_join_observe, + &e) == 0); + CHECK(e.count == (shape ? 0 : 48)); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == + disk); + if (!shape) { + col_rel_t *out = session_find_rel(s, "result"); + CHECK(out && out->column_types && + out->column_types[2] == WIRELOG_TYPE_STRING && + out->column_types[4] == WIRELOG_TYPE_STRING); + } + CHECK(wl_session_step(f.session) == 0); + } + destroy(&f); + } +} + +static const char *inline_projection_query = + ".decl a(k:int64,p:pair(symbol,symbol) inline)\n.decl b(p:pair(symbol,int64) inline)\n" + ".decl result(s:symbol,k:int64,t:symbol)\n.decl other(s:symbol,k:int64)\n" + "result(s,k,s) :- a(k,pair(s,u)), k >= 3.\nother(s,k) :- b(pair(s,k)).\n"; +static fixture_t inline_projection_fixture(uint64_t allowance, unsigned rows) +{ + fixture_t f = fixture(inline_projection_query, allowance, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + int64_t ids[3]; + for (unsigned i = 0; i < 3; i++) { + ids[i] = wl_intern_put(s->intern, reference_values[i]); + CHECK(ids[i] >= 0); + } + for (unsigned i = 0; i < (rows ? rows : 1); i++) { + int64_t a[] = {(int64_t)i, ids[i%3], ids[(i+1)%3]}, + b[] = {ids[i%3], (int64_t)i}; + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + if (!rows) { + CHECK(wl_session_remove(f.session, "a", a, 1, 3) == 0); + CHECK(wl_session_remove(f.session, "b", b, 1, 2) == 0); + } + } + return f; +} +static void inline_projection_failures(void) +{ + for (unsigned fault = 0; fault < 6; fault++) { + fixture_t f = inline_projection_fixture(4*1024*1024, 9); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, NULL) == OK); + col_rel_t *source = session_find_rel(s, "a"); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + int64_t saved = source->columns[2][8]; + wl_intern_t *intern = s->intern, *foreign = NULL; + if (fault == 0) source->columns[2][8] = INT64_MAX; /* Unprojected invalid ID. */ + if (fault == 1) s->intern = NULL; + if (fault == 2) { + foreign = wl_intern_create(); CHECK(foreign); s->intern = foreign; + } + if (fault == 3) allocation_fault = 1; + if (fault == 4) write_fault = 1; + cancellation_t cancellation = {.stop = 3}; + wl_columnar_spill_status_t rc = + wl_columnar_session_seal_input(f.session, "a", + fault == 5 ? cancel : NULL, &cancellation); + CHECK(rc != OK && session_find_rel(s, + "a") == source && !wl_columnar_session_cold_find(s, "a")); + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + s->intern = intern; if (foreign) wl_intern_free(foreign); + source->columns[2][8] = saved; allocation_fault = write_fault = 0; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + const wl_plan_relation_t *rp = join_body(&f, "result"); + eval_stack_t stack; eval_stack_init(&stack); before = reserved(&f); + wl_columnar_ops_test_map_fail_output_alloc = true; + CHECK(wl_columnar_ops_cold_projection(rp, &stack, s) == ENOMEM); + wl_columnar_ops_test_map_fail_output_alloc = false; + CHECK(!stack.top && reserved(&f) == before); + s->intern = NULL; + CHECK(wl_columnar_ops_cold_projection(rp, &stack, + s) == ENOTSUP && !stack.top); + s->intern = intern; + read_fault = 9; unsigned events = 0; + CHECK(wl_session_snapshot(f.session, count_only, + &events) == EIO && events == 0); + read_fault = 0; reference_events_t e = {.intern = intern}; + CHECK(wl_session_snapshot(f.session, reference_observe, + &e) == 0 && e.bits[0] == 504 && e.bits[1] == 511); + destroy(&f); + } +} +static void inline_projection_memory(void) +{ + wl_columnar_memory_governor_test_admitted = budget_admitted; + budget_peak = 0; + fixture_t calibration = inline_projection_fixture(4*1024*1024, 1024); + CHECK(wl_columnar_session_seal_input(calibration.session, "a", NULL, + NULL) == OK); + wl_plan_relation_t wide; wl_plan_op_t ops[4]; uint32_t indices[32]; + reference_wide_projection(&calibration, &wide, ops, indices); + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_ops_cold_projection(&wide, &stack, + COL_SESSION(calibration.session)) == 0); + uint64_t allowance = budget_peak; + eval_entry_t entry = eval_stack_pop(&stack); + CHECK(entry.rel->nrows == 1021 && + entry.rel->column_types[0] == WIRELOG_TYPE_STRING); + CHECK(eval_stack_dispose_entry(&stack, &entry) == 0); destroy(&calibration); + for (unsigned retain = 0; retain < 2; retain++) { + budget_peak = 0; + fixture_t f = inline_projection_fixture(allowance, 1024); + wl_col_session_t *s = COL_SESSION(f.session); + wl_columnar_session_test_retain_cold_source = retain != 0; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + wl_columnar_session_test_retain_cold_source = false; + reference_wide_projection(&f, &wide, ops, indices); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + int rc = wl_columnar_ops_cold_projection(&wide, &stack, s); + CHECK(budget_peak <= allowance); + if (retain) CHECK(rc == ENOSPC && !stack.top && + wl_session_budget_denied(f.session)); + else { + CHECK(rc == 0); entry = eval_stack_pop(&stack); + CHECK(entry.rel->nrows == 1021 && + entry.rel->column_types[0] == WIRELOG_TYPE_STRING); + CHECK(eval_stack_dispose_entry(&stack, &entry) == 0); + } + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + } + wl_columnar_memory_governor_test_admitted = NULL; + /* Intern storage remains resident. Real distinct strings eventually exhaust + * a fixed allowance even though the row source has already been sealed. */ + fixture_t f = inline_projection_fixture(256*1024, 9); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + bool denied = false; + for (unsigned i = 0; i < 16384; i++) { + char value[256]; memset(value, 'x', sizeof(value)); + int length = snprintf(value, 32, "distinct-%u-", i); CHECK(length > 0); + value[length] = 'x'; value[sizeof(value)-1] = '\0'; + int64_t id = -1; + int rc = wl_intern_put_checked(s->intern, value, &id); + if (rc == WL_INTERN_ERR_MEMORY_BUDGET) { + denied = true; break; + } + CHECK(rc == 0 && id >= 0); + } + CHECK(denied && reserved(&f) > before && + wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); +} + +static const char *inline_join_budget_query = + ".decl a(k:int64,v:shared/1 inline,s:symbol)\n.decl b(k:int64,v:shared/1 inline,t:symbol)\n" + ".decl result(k:int64,x:int64,y:int64,s:symbol)\n" + "result(k,x,y+0,s) :- a(k,shared(x),s),b(k,shared(y),t).\n"; +static fixture_t inline_join_budget_fixture(uint64_t allowance, bool both) +{ + fixture_t f = fixture(inline_join_budget_query, allowance, 8*1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_intern_put(s->intern, "shared") == 0); + s->spill_join_batch_bytes = 1024; s->spill_join_index_bytes = 64*1024; + for (int64_t i = 0; i < 4096; i++) { + int64_t a[] = {i < 128 ? 0 : 10000+i, i, 0}; + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + } + for (int64_t i = 0; i < (both ? 4096 : 128); i++) { + int64_t b[] = {i < 128 ? 0 : 20000+i, 100+i, 0}; + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + } + return f; +} +typedef struct { unsigned count; uint64_t seen[256];} inline_reclaimed_events_t; +static void inline_reclaimed_observe(const char *name, const int64_t *row, + uint32_t width, void *context) +{ + inline_reclaimed_events_t *e = context; + CHECK(!strcmp(name, + "result") && width == 4 && row[3] == 0 && row[0] == 0 && row[1] >= 0 && + row[1] < 128 && + row[2] >= 100 && row[2] < 228); + unsigned pos = (unsigned)(row[1]*128 + row[2]-100); + uint64_t bit = UINT64_C(1) << (pos%64); + CHECK(!(e->seen[pos/64]&bit)); e->seen[pos/64] |= bit; e->count++; +} +static void inline_join_reclamation(void) +{ + wl_columnar_memory_governor_test_admitted = budget_admitted; + for (unsigned both = 0; both < 2; both++) { + budget_peak = 0; + fixture_t calibration = inline_join_budget_fixture(8*1024*1024, + both != 0); + seal_direction(&calibration, both ? 3 : 1); + uint64_t preparation = budget_peak; + inline_reclaimed_events_t e = {0}; + CHECK(wl_session_snapshot(calibration.session, + inline_reclaimed_observe, + &e) == 0 && e.count == 16384); + uint64_t allowance = budget_peak; + CHECK(allowance > preparation); + destroy(&calibration); + for (unsigned retain = 0; retain < 2; retain++) { + budget_peak = 0; + fixture_t f = inline_join_budget_fixture(allowance, both != 0); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f); + wl_columnar_session_test_retain_cold_source = retain != 0; + seal_direction(&f, both ? 3 : 1); + wl_columnar_session_test_retain_cold_source = false; + CHECK(retain ? reserved(&f) > before : reserved(&f) < before); + unsigned owners = 0; + for (wl_columnar_session_cold_relation_t *c = s->cold_relation; c; + c = c->next) { + CHECK((c->retained_source != NULL) == (retain != 0)); owners++; + } + CHECK(owners == (both ? 2u : 1u)); + uint64_t disk = wl_columnar_spill_store_bytes(s->spill_store); + e = (inline_reclaimed_events_t){0}; + int rc = wl_session_snapshot(f.session, inline_reclaimed_observe, + &e); + CHECK(budget_peak <= allowance); + if (retain) CHECK(rc == ENOSPC && + wl_session_budget_denied(f.session) && e.count == 0); + else CHECK(rc == 0 && e.count == 16384); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + destroy(&f); + CHECK(!wl_columnar_session_test_has_retained_cold_source(NULL)); + } + } + wl_columnar_memory_governor_test_admitted = NULL; +} +static void inline_join_bounded_scratch(void) +{ + uint64_t peaks[2] = {0}; + for (unsigned large = 0; large < 2; large++) { + fixture_t f = fixture(inline_join_budget_query, 4*1024*1024, + 1024*1024); + CHECK(wl_intern_put(COL_SESSION(f.session)->intern, "shared") == 0); + for (int64_t i = 0; i < (large ? 512 : 64); i++) { + int64_t a[] = {i, 10+i, 0}, b[] = {10000+i, 100+i, 0}; + CHECK(wl_session_insert(f.session, "a", a, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 3) == 0); + } + seal_direction(&f, 3); + wl_col_session_t *s = COL_SESSION(f.session); + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + budget_peak = before; constructor_peak = 0; + wl_columnar_memory_governor_test_admitted = budget_admitted; + wl_columnar_join_test_spill_producer_result = constructor_result; + eval_stack_t stack; eval_stack_init(&stack); + CHECK(wl_columnar_join_cold_left(s, join_body(&f, "result"), + &stack) == 0); + peaks[large] = constructor_peak-before; + CHECK(constructor_peak > before && budget_peak == constructor_peak); + eval_entry_t e; + CHECK(eval_stack_pop_relation(&stack, &e) == 0 && e.rel->nrows == 0); + CHECK(eval_stack_dispose_entry(&stack, + &e) == 0 && reserved(&f) == before); + CHECK(wl_columnar_spill_store_bytes(s->spill_store) == disk); + wl_columnar_memory_governor_test_admitted = NULL; + wl_columnar_join_test_spill_producer_result = NULL; + destroy(&f); + } + CHECK(peaks[0] == peaks[1]); +} + +static void inline_source_refusals(void) +{ + for (unsigned bad = 0; bad < 12; bad++) { + fixture_t f = inline_projection_fixture(4*1024*1024, 9); + wl_col_session_t *s = COL_SESSION(f.session); + CHECK(wl_columnar_session_seal_input(f.session, "b", NULL, NULL) == OK); + col_rel_t *source = session_find_rel(s, "a"); + const wl_plan_edb_schema_t *all = f.plan->edb_schemas; + uint32_t edb = 0; + while (edb < f.plan->edb_count && strcmp(f.plan->edb_relations[edb], + "a")) edb++; + CHECK(edb < f.plan->edb_count); + wl_plan_edb_schema_t *schema = (wl_plan_edb_schema_t *)&all[edb]; + wl_plan_edb_schema_t saved_schema = *schema; + wirelog_column_t *columns = (wirelog_column_t *)schema->columns; + wirelog_column_t saved[2] = {columns[0], columns[1]}; + wirelog_column_type_t saved_type = source->column_types[1]; + uint32_t saved_count = source->compound_count, + saved_offset = source->inline_physical_offset; + uint32_t saved_arity = source->compound_arity_map[1]; + uint64_t before = reserved(&f), + disk = wl_columnar_spill_store_bytes(s->spill_store); + uint64_t generation = source->storage_generation; + if (bad == 0) f.plan->edb_schemas = NULL; + if (bad == 1) schema->has_unsupported_inline_cold_use = true; + if (bad == 2) columns[1].compound_functor_id = UINT32_MAX; + if (bad == 3) columns[1].compound_arity = 0; + if (bad == 4) columns[1].compound_inline_col_offset++; + if (bad == 5) source->compound_arity_map[1]++; + if (bad == 6) source->compound_count = 0; + if (bad == 7) source->inline_physical_offset++; + if (bad == 8) columns[0].type = WIRELOG_TYPE_BOOL; + if (bad == 9) source->column_types[1] = WIRELOG_TYPE_INT64; + if (bad == 10) source->compound_kind = WIRELOG_COMPOUND_KIND_SIDE; + if (bad == 11) schema->column_count--; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + CHECK(reserved(&f) == before && + wl_columnar_spill_store_bytes(s->spill_store) == disk && + session_find_rel(s, + "a") == source && source->storage_generation == generation && + !wl_columnar_session_cold_find(s, + "a") && wl_columnar_session_cold_find(s, "b")); + f.plan->edb_schemas = all; *schema = saved_schema; + columns[0] = saved[0]; columns[1] = saved[1]; + source->column_types[1] = saved_type; + source->compound_count = saved_count; + source->inline_physical_offset = saved_offset; + source->compound_arity_map[1] = saved_arity; + source->compound_kind = WIRELOG_COMPOUND_KIND_INLINE; + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + schema->has_unsupported_inline_cold_use = true; + unsigned count = 0; + CHECK(wl_session_snapshot(f.session, count_only, + &count) == ENOTSUP && !count); + schema->has_unsupported_inline_cold_use = false; + reference_events_t e = {.intern = s->intern}; + CHECK(wl_session_snapshot(f.session, reference_observe, &e) == 0 && + e.bits[0] == 504 && e.bits[1] == 511); + destroy(&f); + } + const char *patterns[] = {"pair(nested(s),n)", "whole", "_", "wrong(s,n)", + "pair(s)"}; + for (unsigned i = 0; i < 5; i++) { + char text[512]; + snprintf(text, sizeof(text), + ".decl a(i:int64,p:pair(symbol,int64) inline)\n" + ".decl out(i:int64)\n.decl safe(i:int64)\n" + "out(i) :- a(i,%s).\nsafe(i) :- a(i,pair(s,n)).\n", patterns[i]); + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + col_rel_t *r = session_find_rel(s, "a"); + uint64_t before = reserved(&f); + CHECK(r && r->compound_kind == WIRELOG_COMPOUND_KIND_INLINE); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, + NULL) == WL_COLUMNAR_SPILL_UNSUPPORTED); + CHECK(session_find_rel(s, + "a") == r && reserved(&f) == before && !s->cold_relation); + destroy(&f); + } +} + +static void numeric_inline_cold_source(void) +{ + const char *text = + ".decl a(id:int64,p:pair/2 inline)\n.decl b(k:int64,v:int64)\n" + ".decl out(id:int64,x:int64,y:int64)\n" + ".decl filtered(id:int64,x:int64,y:int64)\n" + ".decl joined(id:int64,x:int64,y:int64)\n" + "out(i,x,y) :- a(i,pair(x,y)).\n" + "filtered(i,x,y) :- a(i,pair(x,y)), x > 0.\n" + "joined(i,x,y+0) :- a(i,pair(x,y)),b(x,z).\n"; + fixture_t f = fixture(text, 4*1024*1024, 1024*1024); + wl_col_session_t *s = COL_SESSION(f.session); + col_rel_t *a = session_find_rel(s, "a"); + CHECK(a && !a->nrows && a->ncols == 3 && a->column_types && + a->compound_kind == WIRELOG_COMPOUND_KIND_INLINE && + a->compound_count == 1 && a->compound_arity_len == 2 && + a->compound_arity_map[0] == 1 && a->compound_arity_map[1] == 2); + int64_t row[] = {7, 11, 13}, b[] = {11, 17}; + CHECK(wl_session_insert(f.session, "a", row, 1, 3) == 0); + CHECK(wl_session_insert(f.session, "b", b, 1, 2) == 0); + CHECK(wl_columnar_session_seal_input(f.session, "a", NULL, NULL) == OK); + wl_columnar_session_cold_relation_t *cold = wl_columnar_session_cold_find(s, + "a"); + CHECK(cold && wl_columnar_batch_payload_uses_intern(cold->payload, + s->intern)); + uint32_t logical = 0; + const wirelog_column_t *decl = + wl_columnar_batch_payload_inline_schema(cold->payload, &logical); + CHECK(decl && logical == 2 && decl[1].compound_arity == 2 && + decl[1].compound_inline_col_offset == 1); + wl_columnar_batch_payload_reader_t *reader; + CHECK(wl_columnar_batch_payload_acquire(cold->payload, &reader) == OK); + const char *functor = NULL; + CHECK(wl_columnar_batch_payload_resolve_string(reader, + decl[1].compound_functor_id, &functor) == OK && !strcmp(functor, + "pair")); + wl_columnar_batch_payload_reader_release(reader); + for (unsigned repeat = 0; repeat < 2; repeat++) { + unsigned bits = 0; + CHECK(wl_session_snapshot(f.session, inline_initialized_observe, + &bits) == 0 && bits == 7); + const char *names[] = {"out", "filtered", "joined"}; + for (unsigned i = 0; i < 3; i++) { + col_rel_t *out = session_find_rel(s, names[i]); + CHECK(out && out->ncols == 3 && + out->compound_kind == WIRELOG_COMPOUND_KIND_NONE && + !out->compound_count && !out->compound_arity_map); + } + CHECK(wl_session_step(f.session) == 0); + } + destroy(&f); +} + int main(void) { /* Issue #2287: session creation reads WIRELOG_JOIN_BATCH_BYTES and @@ -2612,6 +3064,10 @@ int main(void) * session exists. */ CHECK(!unsetenv("WIRELOG_JOIN_BATCH_BYTES")); CHECK(!unsetenv("WIRELOG_JOIN_BATCH_STRICT")); + inline_source_refusals(); numeric_inline_cold_source(); + inline_projection_failures(); inline_projection_memory(); + inline_join_reclamation(); inline_join_bounded_scratch(); + inline_join_directions(); inline_initialization_budget(); inline_initialized_sources(); reference_join_reclamation(); reference_join_bounded_scratch(); diff --git a/tests/test_wirelog_advanced_spill.c b/tests/test_wirelog_advanced_spill.c index bfe73b1a1..f4a11f9a2 100644 --- a/tests/test_wirelog_advanced_spill.c +++ b/tests/test_wirelog_advanced_spill.c @@ -910,6 +910,108 @@ static void reference_public_joins(void) } } +static void inline_public_projections(void) +{ + for (unsigned filtered = 0; filtered < 2; filtered++) + for (unsigned empty = 0; empty < 2; empty++) + for (unsigned order = 0; order < 3; order++) { + char text[1024]; + snprintf(text, sizeof(text), + ".decl a(p:pair(int64,symbol) inline,q:box(symbol) inline)\n.decl b(p:pair(symbol,int64) inline)\n" + ".decl result(s:symbol,k:int64,t:symbol)\n.decl other(s:symbol,k:int64)\n" + "a(0,\"\",\"unused\").\na(1,\"alpha\",\"unused\").\na(2,\"한글\",\"unused\").\n" + "b(\"\",0).\nb(\"alpha\",1).\nb(\"한글\",2).\n" + "result(s,k,s) :- a(pair(k,s),box(u))%s.\nother(s,k) :- b(pair(s,k)).\n", + filtered ? ", k >= 1" : ""); + wirelog_program_t *p = parse(text); wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, + WIRELOG_BACKEND_DEFAULT, 1, &o, &s) == WIRELOG_OK); + int64_t *facts = NULL; uint32_t rows = 0, cols = 0; + CHECK(wirelog_program_get_facts(p, "a", &facts, &rows, + &cols) == 0 && rows == 3 && cols == 3); + reference_public_events_t expected = {0}; + for (unsigned i = 0; i < rows; i++) { + CHECK(facts[i*cols] >= 0 && facts[i*cols] < 3); + expected.ids[facts[i*cols]] = facts[i*cols+1]; + if (empty) CHECK(wirelog_session_remove(s, "a", + facts+i*cols, 1, cols) == WIRELOG_OK); + } + if (!empty) CHECK(wirelog_session_insert(s, "a", facts, 1, + cols) == WIRELOG_OK); /* Duplicate row. */ + free(facts); facts = NULL; + CHECK(wirelog_program_get_facts(p, "b", &facts, &rows, + &cols) == 0 && rows == 3 && cols == 2); + if (empty) CHECK(wirelog_session_remove(s, "b", facts, rows, + cols) == WIRELOG_OK); + free(facts); + if (order) { + CHECK(wirelog_session_seal_input(s, + order == 1 ? "a" : "b") == WIRELOG_OK); + CHECK(wirelog_session_seal_join_input(s, + order == 1 ? "b" : "a") == WIRELOG_OK); + } + if (order == 2) CHECK(wirelog_session_step(s) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_public_events_t e = expected; + CHECK(wirelog_session_snapshot_typed(s, + reference_public_observe, &e) == WIRELOG_OK); + CHECK(e.bits[0] == (empty ? 0u : filtered ? 6u : 7u) && + e.bits[1] == (empty ? 0u : 7u)); + } + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + +static void inline_public_joins(void) +{ + const char *text = + ".decl a(p:pair(symbol,int64) inline)\n.decl b(p:triple(symbol,symbol,int64) inline)\n" + ".decl c(p:pair(symbol,int64) inline)\n" + ".decl result(k:symbol,x:int64,next:symbol,z:int64)\n" + ".decl self(k:symbol,x:int64,p:symbol,y:int64)\n" + ".decl scan(k:symbol,x:int64)\n" + ".decl second(k:symbol,x:int64,next:symbol,y:int64)\n" + "a(\"\",0).\na(\"alpha\",1).\na(\"한글\",2).\n" + "b(\"\",\"alpha\",10).\nb(\"alpha\",\"한글\",11).\nb(\"한글\",\"\",12).\n" + "c(\"alpha\",100).\nc(\"한글\",101).\nc(\"\",102).\n" + "result(k,x,n,z+0) :- a(pair(k,x)),b(triple(k,n,y)),c(pair(n,z)).\n" + "self(k,x,k,y+0) :- a(pair(k,x)),a(pair(k,y)).\n" + "scan(k,x) :- a(pair(k,x)).\n" + "second(k,x,n,y+0) :- a(pair(k,x)),b(triple(k,n,y)).\n"; + for (unsigned direction = 0; direction < 5; direction++) { + wirelog_program_t *p = parse(text); wirelog_session_t *s = NULL; + wirelog_session_options_v1_t o = configured(); + CHECK(wirelog_session_create_with_options(p, WIRELOG_BACKEND_DEFAULT, + 1, &o, &s) == WIRELOG_OK); + int64_t *facts = NULL; uint32_t rows = 0, cols = 0; + CHECK(wirelog_program_get_facts(p, "a", &facts, &rows, + &cols) == 0 && rows == 3 && cols == 2); + reference_chain_events_t expected = {0}; + for (unsigned i = 0; i < rows; + i++) expected.ids[facts[i*2+1]] = (uint64_t)facts[i*2]; + free(facts); + const char *names[] = {"a", "b", "c"}; + if (direction == 1) CHECK(wirelog_session_seal_input(s, + "a") == WIRELOG_OK); + if (direction == 2) CHECK(wirelog_session_seal_input(s, + "c") == WIRELOG_OK); + if (direction >= 3) for (unsigned i = 0; i < 3; i++) + CHECK(wirelog_session_seal_input(s, + names[direction == 3 ? i : 2-i]) == WIRELOG_OK); + for (unsigned repeat = 0; repeat < 2; repeat++) { + reference_chain_events_t e = expected; + CHECK(wirelog_session_snapshot_typed(s, reference_chain_observe, + &e) == WIRELOG_OK); + for (unsigned i = 0; i < 4; i++) CHECK(e.seen[i] == 7); + CHECK(wirelog_session_step(s) == WIRELOG_OK); + } + wirelog_session_destroy(s); wirelog_program_free(p); + CHECK(live_files() == 0); + } +} + int main(void) { /* Issue #2287: a session created with a nonzero WIRELOG_JOIN_BATCH_BYTES @@ -928,6 +1030,7 @@ int main(void) test_tmpdir()); CHECK(mkdtemp(directory)); #endif + inline_public_projections(); inline_public_joins(); reference_public_joins(); reference_public_projections(); join_directions(); join_chains(); diff --git a/wirelog/columnar/internal.h b/wirelog/columnar/internal.h index 1a21f4490..219200fa7 100644 --- a/wirelog/columnar/internal.h +++ b/wirelog/columnar/internal.h @@ -4937,8 +4937,9 @@ int session_remove_rel(wl_col_session_t *sess, const char *name); /* Explicit internal pre-evaluation opt-in, under external session lifecycle * serialization. Caller keeps the plan immutable through session destruction. - * Canonical numeric EDBs used only by supported right JOINs or complete - * direct/filtered projections may be sealed individually. Success removes its resident registry owner and makes ALL fact and + * Supported scalar or declared flat INLINE EDBs used only by audited JOINs + * or complete direct/filtered projections may be sealed individually. Success + * removes its resident registry owner and makes ALL fact and * compound mutation unsupported, including zero-row requests. Failed sealing * preserves the source/registry and refunds private work. * No readers may escape synchronous operations; callbacks must not reenter @@ -4954,6 +4955,11 @@ wl_columnar_spill_status_t wl_columnar_session_seal_join_input( wl_columnar_session_cold_relation_t *wl_columnar_session_cold_find( const wl_col_session_t *session, const char *name); int wl_columnar_session_cold_execution_check(const wl_col_session_t *session); +/* Read-only source/declaration provenance; returns NULL schema for scalars. */ +bool wl_columnar_session_cold_source_schema(const wl_col_session_t *session, + const char *name, const col_rel_t *resident, + const struct wl_columnar_batch_payload *payload, + const wl_plan_edb_schema_t **schema); /* Valid only after the complete cold projection body has been audited. */ typedef struct { const wl_plan_op_t *map; diff --git a/wirelog/columnar/join.c b/wirelog/columnar/join.c index a8a4de05d..cd3577674 100644 --- a/wirelog/columnar/join.c +++ b/wirelog/columnar/join.c @@ -1499,6 +1499,8 @@ typedef struct { const char *const *names; const wirelog_column_type_t *types; const wl_intern_t *intern; + const wl_plan_edb_schema_t *schema; + bool schema_valid; } wl_columnar_join_source_t; static wl_columnar_join_source_t @@ -1527,6 +1529,9 @@ wl_columnar_join_source(const wl_col_session_t *s, const char *name, src.types = resident->column_types; } } + src.schema_valid = wl_columnar_session_cold_source_schema(s, + name ? name : resident ? resident->name : NULL, src.resident, + src.cold ? src.cold->payload : NULL, &src.schema); return src; } @@ -1542,6 +1547,7 @@ wl_columnar_join_source_key(const wl_columnar_join_source_t *src, static bool wl_columnar_join_source_references(const wl_columnar_join_source_t *src) { + if (src->schema) return true; /* Numeric INLINE still owns functor IDs. */ for (uint32_t c = 0; src->types && c < src->width; c++) if (src->types[c] == WIRELOG_TYPE_STRING) return true; return false; @@ -1551,12 +1557,8 @@ static bool wl_columnar_join_source_valid(const wl_columnar_join_source_t *src, uint32_t key) { - if (key >= src->width || (!src->cold && !src->resident)) return false; - const col_rel_t *r = src->resident; - if (r && (r->timestamps || r->has_graph_column || - r->compound_kind != WIRELOG_COMPOUND_KIND_NONE || r->compound_count || - r->compound_arity_map || r->compound_arity_len || - r->inline_physical_offset)) return false; + if (!src->schema_valid || key >= src->width || + (!src->cold && !src->resident)) return false; wirelog_column_type_t type = src->types ? src->types[key] : WIRELOG_TYPE_INT64; if (type != WIRELOG_TYPE_INT64 && type != WIRELOG_TYPE_STRING) return false; @@ -1575,7 +1577,8 @@ wl_columnar_join_cold_shape(const wl_plan_op_t *op, const wl_columnar_join_source_t *left, const wl_columnar_join_source_t *right) { - if (op->op != WL_PLAN_OP_JOIN || op->key_count != 1 || op->opaque_data || + if (!left->schema_valid || !right->schema_valid || + op->op != WL_PLAN_OP_JOIN || op->key_count != 1 || op->opaque_data || op->materialized || op->right_filter_expr.size || !op->right_relation || (op->project_count && !op->project_indices) || (op->delta_mode != WL_DELTA_AUTO && @@ -1730,7 +1733,15 @@ wl_columnar_join_cold_run(wl_col_session_t *sess, const wl_plan_op_t *op, status = wl_columnar_batch_payload_take(sess->memory_governor, sess->spill_store, &left->rel, NULL, NULL, &inputs[i]); else { - status = + status = sources[i].schema ? + wl_columnar_batch_payload_capture_with_inline( + sess->memory_governor, + sess->spill_store, sources[i].resident, + sources[i].resident->nrows, + WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, + sources[i].schema->columns, + sources[i].schema->column_count, sess->intern, NULL, NULL, + &inputs[i]) : wl_columnar_batch_payload_capture_with_intern( sess->memory_governor, sess->spill_store, sources[i].resident, diff --git a/wirelog/columnar/payload_join.c b/wirelog/columnar/payload_join.c index 824200cd4..23fec4897 100644 --- a/wirelog/columnar/payload_join.c +++ b/wirelog/columnar/payload_join.c @@ -295,8 +295,14 @@ wl_columnar_payload_join_schema(wl_columnar_batch_payload_t *payload, wl_columnar_batch_payload_schema(payload, cols, rows); uint32_t logical; if (!types || key >= *cols) return WL_COLUMNAR_SPILL_INVALID; - if (wl_columnar_batch_payload_inline_schema(payload, &logical) || - (types[key] != WIRELOG_TYPE_INT64 && + const wirelog_column_t *columns = + wl_columnar_batch_payload_inline_schema(payload, &logical); + if (columns && (!intern || !wl_columnar_batch_payload_uses_intern(payload, + intern) || + wl_columnar_spill_validate_inline_schema(types, *cols, columns, + logical) != WL_COLUMNAR_SPILL_OK)) + return WL_COLUMNAR_SPILL_UNSUPPORTED; + if ((types[key] != WIRELOG_TYPE_INT64 && !(intern && types[key] == WIRELOG_TYPE_STRING))) return WL_COLUMNAR_SPILL_UNSUPPORTED; diff --git a/wirelog/columnar/payload_join.h b/wirelog/columnar/payload_join.h index 09c03de57..bebdd8e10 100644 --- a/wirelog/columnar/payload_join.h +++ b/wirelog/columnar/payload_join.h @@ -106,7 +106,10 @@ wl_columnar_spill_status_t wl_columnar_payload_join_create_projected_bounded( /* Reference-aware ordinary projected JOIN. Keys must both be INT64 or both * STRING; every STRING-bearing input must retain the supplied originating * intern table, even for non-key lanes. NULL intern is numeric-only. Legacy - * constructors remain numeric-only; INLINE/weighted/timed inputs are excluded. + * constructors remain numeric-only. Validated flat INLINE payloads may supply + * scalar physical keys/output lanes; numeric-only INLINE still requires its + * originating functor intern table. Output does not inherit source arity maps. + * Weighted/timed inputs remain excluded. * Output STRING IDs borrow the producer readers' retained capability while a * batch is pending. A sink keeping independent payloads must capture with its * own intern pin before acknowledgement (and before original-owner retirement). diff --git a/wirelog/columnar/session.c b/wirelog/columnar/session.c index 80af5037d..eeae92b0f 100644 --- a/wirelog/columnar/session.c +++ b/wirelog/columnar/session.c @@ -615,6 +615,83 @@ wl_columnar_session_cold_find(const wl_col_session_t *s, const char *name) return NULL; } +/* Read-only declaration/source audit shared by seal preflight and consumers. + * Numeric scalar legacy plans need no declaration record. INLINE requires both + * original-use provenance and the real initialized layout, never guessed IDs. */ +bool +wl_columnar_session_cold_source_schema(const wl_col_session_t *s, + const char *name, const col_rel_t *r, + const wl_columnar_batch_payload_t *payload, + const wl_plan_edb_schema_t **out) +{ + *out = NULL; + const wl_plan_edb_schema_t *schema = NULL; + if (s->plan && s->plan->edb_schemas && name) + for (uint32_t i = 0; i < s->plan->edb_count; i++) + if (s->plan->edb_relations && s->plan->edb_relations[i] && + !strcmp(s->plan->edb_relations[i], name)) { + schema = &s->plan->edb_schemas[i]; break; + } + uint32_t logical = 0; + const wirelog_column_t *stored = payload ? + wl_columnar_batch_payload_inline_schema(payload, &logical) : NULL; + bool actual_inline = stored || + (r && (r->compound_kind != WIRELOG_COMPOUND_KIND_NONE || + r->compound_count || r->compound_arity_map || r->compound_arity_len || + r->inline_physical_offset)); + if (r && (r->has_graph_column || r->timestamps)) return false; + if (!schema || !schema->columns || + !schema->column_count) return !actual_inline; + if (schema->has_unsupported_inline_cold_use || !s->intern || + !s->plan || s->intern != s->plan->intern || + (!r && !payload)) return false; + uint32_t width = r ? r->ncols : 0; + const wirelog_column_type_t *types = r ? r->column_types : + wl_columnar_batch_payload_schema(payload, &width, NULL); + if (wl_columnar_spill_validate_inline_schema(types, width, schema->columns, + schema->column_count) != WL_COLUMNAR_SPILL_OK) return false; + if (payload && (!stored || logical != schema->column_count || + !wl_columnar_batch_payload_uses_intern(payload, + s->intern))) return false; + if (r && (r->compound_kind != WIRELOG_COMPOUND_KIND_INLINE || + !r->compound_arity_map || + r->compound_arity_len != schema->column_count)) return false; + uint32_t compounds = 0, first = 0; + for (uint32_t i = 0; i < schema->column_count; i++) { + const wirelog_column_t *c = &schema->columns[i]; + bool inlined = c->compound_kind == WIRELOG_COMPOUND_KIND_INLINE; + uint32_t lanes = inlined ? c->compound_arity : 1; + if (inlined) { + if (!wl_intern_reverse(s->intern, + c->compound_functor_id)) return false; + if (!compounds++) first = c->compound_inline_col_offset; + } + if (r && r->compound_arity_map[i] != lanes) return false; + if (stored && + (stored[i].type != c->type || + stored[i].compound_kind != c->compound_kind || + stored[i].compound_functor_id != c->compound_functor_id || + stored[i].compound_arity != c->compound_arity || + stored[i].compound_inline_col_offset != + c->compound_inline_col_offset)) return false; + } + if (r && + (r->compound_count != compounds || + r->inline_physical_offset != first)) return false; + /* Packed declared child types are authoritative even for unprojected lanes. */ + for (uint32_t i = 0; i < s->plan->edb_count; i++) { + if (&s->plan->edb_schemas[i] != schema) continue; + if (!s->plan->edb_column_type_counts || !s->plan->edb_column_types || + s->plan->edb_column_type_counts[i] != width || + !s->plan->edb_column_types[i]) return false; + for (uint32_t c = 0; c < width; c++) + if (types[c] != s->plan->edb_column_types[i][c]) return false; + break; + } + *out = schema; + return true; +} + /* Eligibility of another rule is independent of the sealing order. Resolve * its own complete scalar schema, never the candidate's schema. Discovery is * read-only: a failed preflight must not allocate a resident registry hash. */ @@ -636,6 +713,9 @@ wl_columnar_session_cold_projection_rule(const wl_col_session_t *s, uint32_t width = 0; const wirelog_column_type_t *types = NULL; if (c) { + const wl_plan_edb_schema_t *schema; + if (!wl_columnar_session_cold_source_schema(s, name, NULL, c->payload, + &schema)) return false; width = c->ncols; types = wl_columnar_batch_payload_schema(c->payload, NULL, NULL); } else { @@ -644,10 +724,9 @@ wl_columnar_session_cold_projection_rule(const wl_col_session_t *s, if (s->rels[i] && !strcmp(s->rels[i]->name, name)) { r = s->rels[i]; break; } - if (!r || !r->ncols || r->timestamps || r->has_graph_column || - r->compound_kind != WIRELOG_COMPOUND_KIND_NONE || - r->compound_count || r->compound_arity_map || - r->compound_arity_len || r->inline_physical_offset) return false; + const wl_plan_edb_schema_t *schema; + if (!r || !r->ncols || !wl_columnar_session_cold_source_schema(s, name, + r, NULL, &schema)) return false; width = r->ncols; types = r->column_types; for (uint32_t i = 0; types && i < width; i++) if (types[i] != WIRELOG_TYPE_INT32 && @@ -756,6 +835,9 @@ wl_columnar_session_cold_execution_check(const wl_col_session_t *s) c = c->next) { const wirelog_column_type_t *types = wl_columnar_batch_payload_schema( c->payload, NULL, NULL); + const wl_plan_edb_schema_t *schema; + if (!wl_columnar_session_cold_source_schema(s, c->name, NULL, + c->payload, &schema)) return ENOTSUP; for (uint32_t i = 0; i < c->ncols; i++) if (types[i] == WIRELOG_TYPE_STRING && !wl_columnar_batch_payload_uses_intern(c->payload, @@ -813,6 +895,9 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, strcmp(s->rels[slot]->name, name) != 0)) slot++; if (slot == s->nrels) return WL_COLUMNAR_SPILL_INVALID; col_rel_t *r = s->rels[slot]; + const wl_plan_edb_schema_t *schema; + if (!wl_columnar_session_cold_source_schema(s, name, r, NULL, &schema)) + return WL_COLUMNAR_SPILL_UNSUPPORTED; if (r->memory_governor != s->memory_governor || !wl_columnar_batch_payload_can_take(r) || r->timestamps || !wl_columnar_session_cold_plan(s, name, r->ncols, @@ -868,7 +953,11 @@ wl_columnar_session_seal_join_input(wl_session_t *session, const char *name, c->cold_storage_generation = r->storage_generation + 1; bool inserted_name = s->last_inserted_relation && strcmp(s->last_inserted_relation, r->name) == 0; - wl_columnar_spill_status_t rc = + wl_columnar_spill_status_t rc = schema ? + wl_columnar_batch_payload_capture_with_inline(s->memory_governor, + s->spill_store, r, r->nrows, WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, + schema->columns, schema->column_count, s->intern, cancel, context, + &c->payload) : wl_columnar_batch_payload_capture_with_intern( s->memory_governor, s->spill_store, r, r->nrows, WL_COLUMNAR_BATCH_PAYLOAD_SPILLED, s->intern, cancel, context, diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index b887ed8f8..f50432438 100644 --- a/wirelog/wirelog-advanced.h +++ b/wirelog/wirelog-advanced.h @@ -168,7 +168,7 @@ wirelog_session_create_with_snapshot(wirelog_program_t *program, * NULL directory and zero spill limits disable spilling. Otherwise the * directory must already exist, quota must be positive, and max_row_bytes * must be at least 32 (wider schemas require more). Both join caps must be - * zero (store only) or positive (enable eligible serial numeric JOINs). + * zero (store only) or positive (enable eligible serial bounded JOINs). * Positive caps require a configured directory. No directories are created. * * Caps bound individual batches/index windows, not total memory. The existing @@ -228,11 +228,17 @@ wirelog_session_create_with_options(wirelog_program_t *program, * A cold left input is consumed by an initial VARIABLE/JOIN pair; both sides * may be sealed, including a self-JOIN. Subsequent JOINs consume an admitted * resident intermediate. No FILTER/MAP before that first cold JOIN, multi-key, - * cross/theta JOIN, other key types, right filter, timestamps, compound values, + * cross/theta JOIN, other key types, right filter, timestamps, whole compound + * values, SIDE/nested layouts, * opaque/materialized rewrites, worker/control/delta modes or delta callback. * JOINs and standalone direct/filtered projections accept STRING lanes from * the originating program intern table, including non-key payload columns. * Both JOIN keys must have the same type; reference IDs are never remapped. + * Declared flat INLINE sources may supply scalar child lanes through these + * projection/JOIN shapes. The initialized layout and original body-use + * provenance must agree with the plan; nested/whole-value/wrong-functor or + * wrong-arity uses make the source ineligible. Outputs are scalar fields, + * not reconstructed compounds. Even numeric INLINE retains its functor table. * The program must outlive the session. Intern storage remains resident, and * sealing validates IDs even in unprojected STRING lanes. * Optimized plans may therefore be ineligible. The session must also not have From d9e38160ee17f8eb0cc0ee297dec538afe6906c5 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 29 Oct 2026 19:41:32 +0900 Subject: [PATCH 9/9] test(columnar): verify scalar reference JOIN output layout --- tests/test_payload_join.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_payload_join.c b/tests/test_payload_join.c index 9122768a7..a77e479ca 100644 --- a/tests/test_payload_join.c +++ b/tests/test_payload_join.c @@ -1508,7 +1508,9 @@ static void reference_joins(bool inlined) types[3] == WIRELOG_TYPE_STRING && types[4] == WIRELOG_TYPE_STRING); CHECK(wl_columnar_batch_payload_uses_intern(out, intern)); - CHECK(!wl_columnar_batch_payload_inline_schema(out, NULL)); + uint32_t logical_count = UINT32_MAX; + CHECK(!wl_columnar_batch_payload_inline_schema(out, + &logical_count) && logical_count == 0); wl_columnar_batch_payload_reader_t *reader; CHECK(wl_columnar_batch_payload_acquire(out, &reader) == OK);