diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e472cb2..a3e899822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,44 @@ 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 + 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. + 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. + +- **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 + 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; + 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..53e05198c 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. @@ -4722,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_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 +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, @@ -4735,17 +4739,39 @@ 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 eligible loaded +EDB can coexist whether that source is resident or already sealed. Auditing uses +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. + 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 -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 +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 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 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 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 @@ -4753,11 +4779,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 @@ -4811,8 +4851,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 +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, delta and controlled paths remain ineligible. The public call is synchronous and has no cancellation callback. Success freezes all fact/compound mutation for the @@ -4888,11 +4930,13 @@ 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).`. 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 @@ -4901,7 +4945,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 @@ -4941,9 +4985,38 @@ 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 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 +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, 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. +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. 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-input subset now has 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 @@ -4952,7 +5025,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. @@ -5045,3 +5118,47 @@ 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. 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 8472eb95a..a70d2cec4 100644 --- a/docs/SEMANTICS.md +++ b/docs/SEMANTICS.md @@ -250,18 +250,27 @@ 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 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 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 +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 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, 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. 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 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. Memory admission, allocation and disk capacity/quota failures retain distinct @@ -295,6 +304,18 @@ 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. 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; 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. + ### References - `wirelog/wirelog-easy.c` — `ensure_plan_built` performs the seed. @@ -1220,7 +1241,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 @@ -1884,3 +1913,23 @@ 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. 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_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_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_payload_join.c b/tests/test_payload_join.c index ab7d67712..a77e479ca 100644 --- a/tests/test_payload_join.c +++ b/tests/test_payload_join.c @@ -1359,8 +1359,201 @@ static void projected(void) } } +static col_rel_t *reference_source(wl_intern_t *intern, unsigned rows, + bool string_key, bool inlined) +{ + 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); + } + 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( + 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; + 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; + 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(bool inlined) +{ + 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, 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, + 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, 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, + 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, 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, + &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)); + 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); + 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(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_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 5bba92a7d..c4f404789 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) { @@ -1217,6 +1230,1831 @@ 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); + } +} + +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_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. */ + 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); +} + +/* 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); + } +} + +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); +} + +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]); +} + +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); + } +} + +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 @@ -1226,6 +3064,27 @@ 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(); + reference_join_directions(); + reference_projection_memory(); + reference_projections(); reference_projection_failures(); + reference_projection_bytecode(); + 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(); + multiple_bindings(); multiple_seal_failures(); + 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_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 aec5a9bf3..f4a11f9a2 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(); @@ -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." }; @@ -552,6 +552,466 @@ 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); + } +} + +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); + } +} + +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); + } +} + +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); + } +} + +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); + } +} + +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 @@ -570,8 +1030,13 @@ 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(); descriptors(); lifecycle(); refusals(); typed_projection(); inline_facts(); - output_floor(); mixed_projection(); unsupported_filters(); + sealed_sets(); standalone_reads(); output_floor(); mixed_projection(); + unsupported_filters(); join_batch_knob(); #ifdef _WIN32 CHECK(_rmdir(directory) == 0); 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 49d40db79..e68619d3c 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; @@ -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; @@ -465,26 +468,32 @@ 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; - 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_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)) 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); return rc ? rc : col_op_consolidate(stack, sess); } } 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/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 fa61f39cc..219200fa7 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,18 +4937,29 @@ 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 + * 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 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); +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; @@ -5302,6 +5316,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); @@ -5475,6 +5495,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 d9a87c17d..cd3577674 100644 --- a/wirelog/columnar/join.c +++ b/wirelog/columnar/join.c @@ -1490,43 +1490,204 @@ 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; + 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 +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; + 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; + } + } + 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; +} + +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_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; +} + +static bool +wl_columnar_join_source_valid(const wl_columnar_join_source_t *src, + uint32_t key) +{ + 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; + 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 +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 (!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 && + 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; + 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++) + 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 = sess->cold_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); @@ -1542,37 +1703,69 @@ 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; + 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 = 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, + 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_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); @@ -1580,10 +1773,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, @@ -1596,6 +1793,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) @@ -1619,9 +1828,8 @@ 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) - return wl_columnar_join_cold_run(sess, op, stack, &left_e); + if (wl_columnar_session_cold_find(sess, op->right_relation)) + 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/ops.c b/wirelog/columnar/ops.c index 6f5468785..18d572cd9 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; @@ -242,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. */ @@ -262,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 @@ -359,15 +362,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/payload_join.c b/wirelog/columnar/payload_join.c index 771e7f846..23fec4897 100644 --- a/wirelog/columnar/payload_join.c +++ b/wirelog/columnar/payload_join.c @@ -289,17 +289,27 @@ 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; + 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; 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 +319,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 +329,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 +478,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 +490,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 +502,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 +515,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 +529,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..bebdd8e10 100644 --- a/wirelog/columnar/payload_join.h +++ b/wirelog/columnar/payload_join.h @@ -103,6 +103,25 @@ 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. 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). + * 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 8f0bc43da..eeae92b0f 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,142 @@ 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; +} + +/* 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. */ +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) { + 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 { + 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; + } + 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 && + 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 && + !(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); +} + /* Cold bindings never enter the raw relation registry. Only an audited, * immutable plan may access them through bounded JOIN/projection readers. */ static bool @@ -615,6 +750,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] && @@ -627,8 +767,15 @@ 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); + bool projection = wl_columnar_session_cold_projection_rule(sess, + rp); + bool selected_projection = projection && + !strcmp(rp->ops[0].relation_name, name); + 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 @@ -641,19 +788,23 @@ 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 || (selected_join && oi == 0))) return false; if (!op->right_relation || strcmp(op->right_relation, name) != 0) continue; - if (op->op != WL_PLAN_OP_JOIN || op->key_count != 1 || + if (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, 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; } } @@ -679,12 +830,22 @@ 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); + 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, + s->intern)) return ENOTSUP; + if (!wl_columnar_session_cold_plan(s, c->name, c->ncols, + (const char *const *)c->col_names, types)) return ENOTSUP; + } + return 0; } static void @@ -709,10 +870,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 || @@ -723,13 +886,18 @@ 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] || 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, @@ -785,9 +953,15 @@ 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 = 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, 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) { @@ -815,17 +989,25 @@ 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; } +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) { @@ -3881,6 +4063,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) @@ -4336,17 +4586,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) @@ -4359,6 +4611,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 @@ -4668,9 +4927,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 @@ -4766,8 +5029,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/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/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 */ diff --git a/wirelog/wirelog-advanced.h b/wirelog/wirelog-advanced.h index 40d818ae5..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 @@ -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,25 +214,43 @@ 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 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. - * 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 input with + * 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 * 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. + * 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, 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 * 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 @@ -246,6 +265,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) {