diff --git a/tests/test_plan_gen.c b/tests/test_plan_gen.c index a69bb66c..9ec10346 100644 --- a/tests/test_plan_gen.c +++ b/tests/test_plan_gen.c @@ -8,6 +8,7 @@ #include "../wirelog/exec_plan_gen.h" #include "../wirelog/backend.h" #include "../wirelog/columnar/columnar_nanoarrow.h" +#include "../wirelog/intern.h" #include "../wirelog/ir/program.h" #include "../wirelog/passes/fusion.h" #include "../wirelog/passes/jpp.h" @@ -133,6 +134,36 @@ test_tc_plan_generation(void) PASS(); } +static void +test_plan_generation_preinterns_static_string_literals(void) +{ + TEST("plan generation pre-interns static string literals"); + + const char *src = ".decl trigger(value: symbol)\n" + ".decl unused(value: symbol, plane: symbol)\n" + "unused(X, \"data\") :- trigger(X).\n"; + + wirelog_error_t err; + wirelog_program_t *prog = wirelog_parse_string(src, &err); + ASSERT(prog != NULL, "parse failed"); + ASSERT(wl_intern_get(prog->intern, "data") < 0, + "parser unexpectedly interned projection literal"); + + ASSERT(wl_fusion_apply(prog, NULL) == 0, "fusion failed"); + ASSERT(wl_jpp_apply(prog, NULL) == 0, "JPP failed"); + ASSERT(wl_sip_apply(prog, NULL) == 0, "SIP failed"); + + wl_plan_t *plan = NULL; + int rc = wl_plan_from_program(prog, &plan); + ASSERT(rc == 0, "plan generation failed"); + ASSERT(wl_intern_get(prog->intern, "data") >= 0, + "plan generation did not intern projection literal"); + + wl_plan_free(plan); + wirelog_program_free(prog); + PASS(); +} + /* ---------------------------------------------------------------- * Test: TC end-to-end via columnar session * ---------------------------------------------------------------- */ @@ -367,6 +398,7 @@ main(void) printf("=== Plan Generator Tests ===\n"); test_tc_plan_generation(); + test_plan_generation_preinterns_static_string_literals(); test_tc_end_to_end(); test_join_project_fusion_plan_shape(); test_join_project_fusion_keeps_computed_map(); diff --git a/tests/test_wirelog_easy.c b/tests/test_wirelog_easy.c index e7de32e0..0fbeb63b 100644 --- a/tests/test_wirelog_easy.c +++ b/tests/test_wirelog_easy.c @@ -544,6 +544,84 @@ test_insert_step_delta(void) PASS(); } +/* PARITY: facade-only -- advanced facade has no eager-open API or + * session-scoped public symbol interning API (#932). */ +static void +test_eager_sessions_preintern_projection_literals(void) +{ + TEST("eager sessions pre-intern projection literals"); + + const char *src = ".decl trigger(value: symbol)\n" + ".decl unused(value: symbol, plane: symbol)\n" + "unused(X, \"data\") :- trigger(X).\n"; + wirelog_easy_open_opts_t opts = WIRELOG_EASY_OPEN_OPTS_INIT; + opts.eager_build = true; + wirelog_easy_session_t *read_session = NULL; + wirelog_easy_session_t *delta_session = NULL; + + if (wirelog_easy_open_opts(src, &opts, &read_session) != WIRELOG_OK + || !read_session + || wirelog_easy_open_opts(src, &opts, &delta_session) != WIRELOG_OK + || !delta_session) { + FAIL("eager open failed"); + wirelog_easy_close(delta_session); + wirelog_easy_close(read_session); + return; + } + + int64_t read_seed = wirelog_easy_intern(read_session, "seed"); + int64_t delta_seed = wirelog_easy_intern(delta_session, "seed"); + if (read_seed < 0 || read_seed != delta_seed) { + FAIL("identical eager sessions assigned different seed ids"); + wirelog_easy_close(delta_session); + wirelog_easy_close(read_session); + return; + } + + delta_collector_t deltas; + memset(&deltas, 0, sizeof(deltas)); + if (wirelog_easy_set_delta_cb(delta_session, collect_delta, &deltas) + != WIRELOG_OK + || wirelog_easy_insert(read_session, "trigger", &read_seed, 1) + != WIRELOG_OK + || wirelog_easy_insert(delta_session, "trigger", &delta_seed, 1) + != WIRELOG_OK + || wirelog_easy_step(delta_session) != WIRELOG_OK) { + FAIL("incremental projection evaluation failed"); + wirelog_easy_close(delta_session); + wirelog_easy_close(read_session); + return; + } + + int64_t read_late = wirelog_easy_intern(read_session, "late"); + int64_t delta_late = wirelog_easy_intern(delta_session, "late"); + if (read_late < 0 || read_late != delta_late) { + FAIL("runtime projection literal changed later symbol ids"); + wirelog_easy_close(delta_session); + wirelog_easy_close(read_session); + return; + } + + int64_t data = wirelog_easy_intern(delta_session, "data"); + bool found = false; + for (int i = 0; i < deltas.count; i++) { + if (strcmp(deltas.relations[i], "unused") == 0 + && deltas.ncols[i] == 2 && deltas.rows[i][0] == delta_seed + && deltas.rows[i][1] == data && deltas.diffs[i] == 1) { + found = true; + break; + } + } + + wirelog_easy_close(delta_session); + wirelog_easy_close(read_session); + if (!found) { + FAIL("expected +unused(seed,data) delta not seen"); + return; + } + PASS(); +} + static void test_inline_compound_body_binding(void) { @@ -1920,6 +1998,7 @@ main(void) test_num_workers_explicit_four(); test_intern_returns_same_id(); test_insert_step_delta(); + test_eager_sessions_preintern_projection_literals(); test_inline_compound_body_binding(); test_inline_compound_body_join_binding(); test_inline_compound_functor_mismatch_is_empty(); diff --git a/wirelog/exec_plan_gen.c b/wirelog/exec_plan_gen.c index 691c01f9..e6cbada9 100644 --- a/wirelog/exec_plan_gen.c +++ b/wirelog/exec_plan_gen.c @@ -12,6 +12,7 @@ #include "exec_plan_gen.h" #include "columnar/columnar_nanoarrow.h" +#include "intern.h" #include "ir/ir.h" #include "ir/program.h" #include "wirelog-ir.h" @@ -71,6 +72,76 @@ make_delta_name(const char *name) return d; } +/* + * Intern every static string literal that the plan evaluator may encounter + * before a session starts evaluating expressions. Otherwise evaluation + * order can assign different symbol IDs to otherwise identical sessions. + */ +static int +wl_exec_plan_gen_preintern_expr( + wl_intern_t *intern, const wl_ir_expr_t *expr) +{ + if (!expr) + return 0; + + for (uint32_t i = 0; i < expr->child_count; i++) { + if (wl_exec_plan_gen_preintern_expr(intern, expr->children[i]) != 0) + return -1; + } + + if (expr->type == WL_IR_EXPR_CONST_STR + && (!expr->str_value || wl_intern_put(intern, expr->str_value) < 0)) + return -1; + + return 0; +} + +static int +wl_exec_plan_gen_preintern_node( + wl_intern_t *intern, const wirelog_ir_node_t *node) +{ + if (!node) + return 0; + + for (uint32_t i = 0; i < node->child_count; i++) { + if (wl_exec_plan_gen_preintern_node(intern, node->children[i]) != 0) + return -1; + } + + if (wl_exec_plan_gen_preintern_expr(intern, node->filter_expr) != 0) + return -1; + for (uint32_t i = 0; node->project_exprs && i < node->project_count; i++) { + if (wl_exec_plan_gen_preintern_expr(intern, node->project_exprs[i]) + != 0) + return -1; + } + if (wl_exec_plan_gen_preintern_expr(intern, node->agg_expr) != 0) + return -1; + + /* + * Compound-inline arguments and compound-side handles are excluded: + * translate_ir_node() does not serialize those expression fields for the + * current plan evaluator. + */ + return 0; +} + +static int +wl_exec_plan_gen_preintern_static_strings( + const struct wirelog_program *prog) +{ + if (!prog->intern) + return -1; + + for (uint32_t i = 0; i < prog->relation_count; i++) { + if (wl_exec_plan_gen_preintern_node( + prog->intern, prog->relation_irs ? prog->relation_irs[i] : NULL) + != 0) + return -1; + } + return 0; +} + /* ======================================================================== */ /* Expression serialization (IR expr tree -> postfix byte buffer) */ /* ======================================================================== */ @@ -2516,6 +2587,9 @@ wl_plan_from_program(const struct wirelog_program *prog, wl_plan_t **out) *out = NULL; + if (wl_exec_plan_gen_preintern_static_strings(prog) != 0) + return -1; + wl_plan_t *plan = (wl_plan_t *)calloc(1, sizeof(wl_plan_t)); if (!plan) return -1;