Skip to content

Commit 9467cc5

Browse files
committed
fix(eql): re-pin the workspace-keyed SteVec selector, and guard its sibling
The SteVec selectors the sqlx suite pins are MACs of (column context, JSONPath) under the CipherStash workspace keyset. This repo's CI names a different workspace than the one EQL was developed against, so the pins re-pinned on the move — no Rust, no SQL and no fixture logic changed. v3_doc_integer::SELECTOR fce8be75… -> 606a4a44…, the value two independent runs reported. Its drift guard did its job: one copy-pasteable message naming the pinned value, the live value and the cause. Its sibling SEL_HELLO_OP is keyed identically and had no such guard, so its drift surfaces as wrong ANSWERS — LB3 counting 0 distinct ops, ORDER BY arms returning insertion order — with the live value nowhere in the output. v3_jsonb_sel_hello_op_matches_fixture closes that asymmetry. It prints every op-carrying selector with a row count and op-length profile rather than inferring the replacement: guessing wrong re-pins to the wrong leaf silently, which is the bug already in SEL_HELLO_OP's history, where it named $.number while claiming $.hello and survived because equality-only suites cannot separate them. The shards also ran fail-fast, so shard 1 reported 11 failures and skipped 643 of its 710 tests — one environmental fault answered one question per CI round trip. sqlx-partition.sh now passes --no-fail-fast, which is nextest's own suggestion in that output; the shards run ~5s tests in parallel, so a fully-failing shard still finishes in minutes. Recorded in the plan, including the consequence that these pins are now coupled to this repo's CI workspace, and that selectors.rs holds five more workspace-keyed constants with zero consumers and no guard.
1 parent 497a43d commit 9467cc5

5 files changed

Lines changed: 112 additions & 1 deletion

File tree

‎docs/plans/2026-08-13-eql-monorepo-absorption.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ EQL's credentialed jobs (`test-eql.yml` lines 133–136 and 601–604) reach Cip
144144

145145
There is also a convention mismatch. EQL reads all four values from `secrets`; this repo reads `CS_WORKSPACE_CRN` and `CS_CLIENT_ID` from `vars` and only the key material from `secrets` (`integration-supabase.yml`, `integration-prisma-next.yml`, `prisma-next-e2e.yml`). Port to this repo's convention or `require-cs-secrets` will report the CRN missing.
146146

147+
### The credentials change WORKSPACE, not just variable plumbing — and two test constants are keyed to the workspace
148+
149+
The finding above treats the credentials as a wiring problem. They are also an *identity* problem, which the first credentialed run found and this plan had not anticipated: `vars.CS_WORKSPACE_CRN` here names a different CipherStash workspace than the one EQL was developed against. Two pinned SteVec selectors are MACs of (column context, JSONPath) **under the workspace keyset**, so they re-pin on the move even though no Rust, no SQL and no fixture logic changed:
150+
151+
- `tests/sqlx/src/fixtures/v3_doc_integer.rs::SELECTOR` — `fce8be75…` → `606a4a44…`, reported identically by two independent runs.
152+
- `tests/sqlx/src/fixtures/v3_ste_vec.rs::SEL_HELLO_OP` — same keying, value not yet observed.
153+
154+
This is a known, accepted property rather than a defect: the module comment says supporting multiple workspaces "would require runtime selector resolution, which the static `ScalarType::column_expr()` seam cannot do — out of scope here." The consequence to record is that **these pins are now coupled to this repo's CI workspace**, so rotating `CS_WORKSPACE_CRN` re-pins them again, and a contributor running the suite against their own workspace will see the drift message and must not commit their local value.
155+
156+
Three things this cost, all worth fixing rather than absorbing:
157+
158+
1. **Only one of the two constants had a drift guard.** `v3_doc_integer` failed with one copy-pasteable "pinned X, live Y" message. `SEL_HELLO_OP` has no such guard, so its drift surfaces as wrong ANSWERS — `LB3` counting 0 distinct ops, ORDER BY arms returning insertion order — with the live value nowhere in the output. `v3_jsonb_sel_hello_op_matches_fixture` closes that asymmetry. It deliberately does **not** infer the replacement: it prints every op-carrying selector with a row count and `op`-length profile and states the discriminator, because guessing wrong re-pins to the wrong leaf silently — the exact bug already recorded in `SEL_HELLO_OP`'s own history, where it named `$.number` while claiming `$.hello` and survived because equality-only suites cannot separate the two.
159+
2. **The shards ran fail-fast**, so shard 1 reported 11 failures and skipped 643 of its 710 tests. One environmental fault therefore answered one question per CI round trip. `tasks/test/sqlx-partition.sh` now passes `--no-fail-fast` (nextest's own suggestion in that output); the cost is bounded because the shards run ~5s tests in parallel.
160+
3. **`tests/sqlx/src/selectors.rs` holds five more workspace-keyed constants (`ROOT`, `HELLO`, `N`, `ARRAY_ELEMENTS`, `ARRAY_ROOT`) with zero consumers** — `rg 'Selectors::'` finds no use outside their own definition — and no guard. They are stale against this workspace and nothing says so. Delete them or guard them before anything starts using them; a dead constant that is silently wrong is worse than a missing one. Not done here: it is unrelated to the failure and belongs in its own change.
161+
162+
The other selector-shaped constants in the tree are **synthetic and unaffected** — `crates/eql-bindings/tests/*` and `tests/sqlx/tests/payload_schema_tests.rs` pair theirs with hand-made ciphertext literals and touch no keyset, and the `00000…01` / `deadbeef…` / `ffffffff…` literals in `v3_jsonb_tests.rs` are forged by construction.
163+
147164
### Three imported release workflows violate the no-caching rule — by omission, not by setting
148165

149166
The global constraint is that publish workflows never restore the Actions cache. `release-plz.yml` complies explicitly:

‎packages/eql/tasks/test/sqlx-partition.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ test -f "${ARCHIVE_PATH}" \
3737
# the prebuilt binaries.
3838
echo "==> running nextest partition hash:${SHARD}/${SHARD_TOTAL} from ${ARCHIVE_PATH}"
3939
cd tests/sqlx
40+
# --no-fail-fast: this suite is sharded, and one environmental fault is enough to
41+
# blank a shard. When the workspace-keyed SteVec selector drifted on the move to
42+
# `cipherstash/stack`, shard 1 reported 11 failures and skipped 643 of its 710
43+
# tests — so the run answered "the selector is wrong" and nothing else, and every
44+
# further question cost another full CI round trip. Running to completion turns
45+
# that into one report per push. The cost is bounded: the shards run ~5s tests in
46+
# parallel, so a fully-failing shard finishes in a couple of minutes.
4047
cargo nextest run \
4148
--archive-file "${ARCHIVE_PATH}" \
4249
--workspace-remap "${REPO_ROOT}" \
50+
--no-fail-fast \
4351
--partition "hash:${SHARD}/${SHARD_TOTAL}"

‎packages/eql/tests/sqlx/snapshots/v3_jsonb_tests.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ v3_jsonb_root_gte_blocker
6262
v3_jsonb_root_lt_blocker
6363
v3_jsonb_root_lte_blocker
6464
v3_jsonb_root_neq_blocker
65+
v3_jsonb_sel_hello_op_matches_fixture
6566
v3_jsonb_single_entry_containment_is_blocked
6667
v3_jsonb_ste_vec_entry_payload_check
6768
v3_jsonb_ste_vec_query_payload_check

‎packages/eql/tests/sqlx/src/fixtures/v3_doc_integer.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,17 @@ const FIELD: &str = "field";
4646
/// derived from the workspace keyset + the fixed STE_VEC_PREFIX + the `field`
4747
/// path, so it is stable for a given CipherStash workspace; if it drifts,
4848
/// regenerate the fixture and re-pin from the emitted `"s"`.
49-
pub const SELECTOR: &str = "fce8be759db230351b10a058b7ba50a7";
49+
///
50+
/// **Pinned to this repo's CI workspace** (`vars.CS_WORKSPACE_CRN`), which is
51+
/// not the workspace the suite was developed against. The value below replaced
52+
/// `fce8be759db230351b10a058b7ba50a7` when the suite moved here from
53+
/// `cipherstash/encrypt-query-language`: the selector is keyed by the workspace
54+
/// keyset, so absorbing the repo re-pinned it even though no Rust, no SQL and
55+
/// no fixture logic changed. That is a property of the design, not a defect —
56+
/// `jsonb_entry_integer_selector_matches_fixture` exists precisely to report it
57+
/// as one copy-pasteable message. A contributor running the suite against their
58+
/// own workspace will see that message and should NOT commit their local value.
59+
pub const SELECTOR: &str = "606a4a449a26442d80915f78d142694f";
5060

5161
/// Build the plaintext documents: `{"field": <value>}` per integer fixture value,
5262
/// paired with the bare integer oracle value.

‎packages/eql/tests/sqlx/tests/v3_jsonb_tests.rs‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,81 @@ async fn v3_jsonb_ord_ope_term_entry_branches(pool: PgPool) -> anyhow::Result<()
649649
Ok(())
650650
}
651651

652+
// ============================================================================
653+
// Drift guard for the workspace-keyed `$.hello` op selector.
654+
//
655+
// `SEL_HELLO_OP` is a MAC of (column context, JSONPath) under the workspace
656+
// keyset, so regenerating the fixture against a different CipherStash workspace
657+
// re-pins it. Its sibling `v3_doc_integer::SELECTOR` has carried such a guard
658+
// since upstream; this constant did not, and the asymmetry has a cost: when the
659+
// suite moved to `cipherstash/stack` the workspace changed, every
660+
// `payload -> SEL_HELLO_OP` lookup returned NULL, and the suite reported wrong
661+
// ANSWERS — LB3 counting 0 distinct ops, ordering arms seeing unordered rows —
662+
// rather than the one wrong CONSTANT behind all of them.
663+
//
664+
// The live selector cannot be derived without credentials, so on failure this
665+
// prints every op-carrying selector with the profile needed to pick the right
666+
// one. It deliberately does NOT choose automatically: guessing wrong would
667+
// re-pin to the wrong leaf silently, which is the exact bug recorded in
668+
// `v3_ste_vec::SEL_HELLO_OP`'s history (it once named `$.number` while claiming
669+
// `$.hello`, and survived because equality-only suites cannot separate them).
670+
//
671+
// `$.hello` is the selector on all 10 rows whose `op` length takes exactly TWO
672+
// values in a 9/1 split: a string op term is 8 * (len + 1) + 1 bits, and the
673+
// values are `world-1`..`world-9` (equal length) plus the one-longer
674+
// `world-10`. `$.number` and `$.large` are fixed-width numeric terms (one
675+
// length), `$.accented` spans four, and `$.empty` / `$.nested.deep` are
676+
// constant.
677+
// ============================================================================
678+
679+
#[sqlx::test(fixtures(path = "../fixtures", scripts("v3_ste_vec")))]
680+
async fn v3_jsonb_sel_hello_op_matches_fixture(pool: PgPool) -> anyhow::Result<()> {
681+
let carrying: i64 = sqlx::query_scalar(&format!(
682+
"SELECT count(*) FROM fixtures.v3_ste_vec f \
683+
WHERE EXISTS ( \
684+
SELECT 1 FROM jsonb_array_elements(f.payload::jsonb->'sv') e \
685+
WHERE e->>'s' = '{SEL_HELLO_OP}' AND e ? 'op' \
686+
)"
687+
))
688+
.fetch_one(&pool)
689+
.await?;
690+
691+
if carrying == 10 {
692+
return Ok(());
693+
}
694+
695+
let menu: Vec<(String, i64, i64, i64, i64)> = sqlx::query_as(
696+
"SELECT e->>'s', \
697+
count(DISTINCT f.id), \
698+
count(DISTINCT length(e->>'op')), \
699+
min(length(e->>'op'))::bigint, \
700+
max(length(e->>'op'))::bigint \
701+
FROM fixtures.v3_ste_vec f, jsonb_array_elements(f.payload::jsonb->'sv') e \
702+
WHERE e ? 'op' \
703+
GROUP BY 1 ORDER BY 1",
704+
)
705+
.fetch_all(&pool)
706+
.await?;
707+
708+
let table = menu
709+
.iter()
710+
.map(|(sel, rows, lengths, lo, hi)| {
711+
format!(" {sel} rows={rows} distinct_op_lengths={lengths} min={lo} max={hi}")
712+
})
713+
.collect::<Vec<_>>()
714+
.join("\n");
715+
716+
anyhow::bail!(
717+
"v3_ste_vec $.hello op-selector drifted from the pinned constant.\n \
718+
pinned v3_ste_vec::SEL_HELLO_OP = {SEL_HELLO_OP}\n \
719+
carries `op` on {carrying}/10 rows (want 10)\n \
720+
live op-carrying selectors:\n{table}\n\
721+
The SteVec selector is keyed by the CipherStash workspace. Pick the entry with \
722+
rows=10 and distinct_op_lengths=2 (the 9/1 `world-1..9` vs `world-10` split), \
723+
re-pin SEL_HELLO_OP to it, and re-run.",
724+
)
725+
}
726+
652727
/// LB1–LB3 structural invariants of the GENERATED fixture, asserted directly
653728
/// (the containment / index oracles only imply them). This is the "generated
654729
/// fixture matches the load-bearing properties" guard, and it doubles as the

0 commit comments

Comments
 (0)