diff --git a/.changeset/eql-domains-refuse-null-version.md b/.changeset/eql-domains-refuse-null-version.md new file mode 100644 index 000000000..3f9bcabaf --- /dev/null +++ b/.changeset/eql-domains-refuse-null-version.md @@ -0,0 +1,5 @@ +--- +'@cipherstash/eql': patch +--- + +The stored and query-operand domains (`eql_v3_*`, `eql_v4_*`, `eql_v3.query_*`, `eql_v4.query_*`) now refuse a payload whose `v` is JSON `null`. The CHECK compared `v` with the bundle's version, and a comparison with `null` is NULL, which a CHECK lets through, so such a payload fitted both a v3 and a v4 domain. The range domains already refused it. A NULL value still fits every domain. diff --git a/.changeset/stack-auth-crate-0-44.md b/.changeset/stack-auth-crate-0-44.md new file mode 100644 index 000000000..d82232e33 --- /dev/null +++ b/.changeset/stack-auth-crate-0-44.md @@ -0,0 +1,5 @@ +--- +'@cipherstash/auth': patch +--- + +The native binaries are built from the `stack-auth` crate at its new version, 0.44.0 (it was 0.43.0). The crate changes that version carries are the ones this release already lists; the version number is what lets the crate publish to crates.io beside them. The package's API is unchanged. diff --git a/Cargo.lock b/Cargo.lock index 390a49aad..abfdda925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3321,7 +3321,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "axum", @@ -3463,7 +3463,7 @@ dependencies = [ [[package]] name = "stack-kms" -version = "0.1.0" +version = "0.2.0" dependencies = [ "async-mutex", "base16ct", @@ -3498,7 +3498,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/Cargo.toml b/Cargo.toml index 340cf5e3b..d3234daee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,8 +69,8 @@ incremental = true # but cannot subtract them, and `stack-kms` / `stack-encrypt` need stack-auth # without `http`. Consumers that want the default transport re-enable it with # `features = ["http"]`. -stack-auth = { path = "./packages/stack-auth", version = "0.43.0", default-features = false } -stack-profile = { path = "./packages/stack-profile", version = "0.43.0" } +stack-auth = { path = "./packages/stack-auth", version = "0.44.0", default-features = false } +stack-profile = { path = "./packages/stack-profile", version = "0.44.0" } # Suite crates, from crates.io; Cargo.lock holds the exact versions. # cts-common and zerokms-protocol take caret requirements because the diff --git a/languages/golang/auth/guest/Cargo.lock b/languages/golang/auth/guest/Cargo.lock index fd1ccb4c5..0271e2bda 100644 --- a/languages/golang/auth/guest/Cargo.lock +++ b/languages/golang/auth/guest/Cargo.lock @@ -1829,7 +1829,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "base64", @@ -1886,7 +1886,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 21fc3b08d..f0890b273 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -2132,7 +2132,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "base64", @@ -2232,7 +2232,7 @@ dependencies = [ [[package]] name = "stack-kms" -version = "0.1.0" +version = "0.2.0" dependencies = [ "base16ct", "base64ct", @@ -2260,7 +2260,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/languages/golang/encrypt/postgres_test.go b/languages/golang/encrypt/postgres_test.go index de42f913a..8cc2448fb 100644 --- a/languages/golang/encrypt/postgres_test.go +++ b/languages/golang/encrypt/postgres_test.go @@ -111,8 +111,13 @@ func TestQueriesRunAgainstPostgres(t *testing.T) { if err != nil { t.Fatal(err) } + // reltuples is -1, an unknown count, until the table is analysed; the + // filter gives no row while it is. + if _, err := conn.ExecContext(ctx, `ANALYZE profiles`); err != nil { + t.Fatal(err) + } var rows uint64 - if err := conn.QueryRowContext(ctx, `SELECT greatest(reltuples, 0)::bigint FROM pg_class WHERE oid = 'profiles'::regclass`).Scan(&rows); err != nil { + if err := conn.QueryRowContext(ctx, `SELECT reltuples::bigint FROM pg_class WHERE oid = 'profiles'::regclass AND reltuples >= 0`).Scan(&rows); err != nil { t.Fatal(err) } limit, err := q.TargetLimit(ctx, 10, encrypt.Stats{Rows: rows}) diff --git a/languages/golang/encrypt/query.go b/languages/golang/encrypt/query.go index 115f246c7..f2c018222 100644 --- a/languages/golang/encrypt/query.go +++ b/languages/golang/encrypt/query.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + "math" "github.com/cipherstash/stack/languages/golang/internal/record" "github.com/cipherstash/vitaminc/bindings/go/vcffi" @@ -60,8 +61,12 @@ type Bucket struct { // size the fetch at. None of it is secret: the server holds the filters and // the catalog holds the row count. type Stats struct { - // Rows is the rows the query scans, such as greatest(reltuples, 0) - // from pg_class. + // Rows is the rows the query scans, such as reltuples from pg_class + // read with a reltuples >= 0 filter. reltuples is -1 for a table never + // vacuumed or analysed: that is an unknown count, not an empty table, + // and a count of 0 would promise a fetch of exactly want. While the + // count is unknown, call no TargetLimit and page with a cursor, or run + // ANALYZE first. Rows uint64 // Histogram is a measured histogram of the stored filters' set-bit // counts. With none, the engine uses the index's declared prior, and @@ -101,12 +106,14 @@ type Limit struct { func (l Limit) Rows() (uint64, bool) { return l.rows, l.bounded } // FetchOr is the rows to fetch, or page when no finite fetch can be -// promised, for a caller that pages either way. +// promised, for a caller that pages either way. Never more than +// math.MaxInt64, the largest LIMIT PostgreSQL and database/sql take: a +// larger page is cut to it. func (l Limit) FetchOr(page uint64) uint64 { if l.bounded { return l.rows } - return page + return min(page, math.MaxInt64) } // QueryTerm derives the query term for one field of the plan, an operator diff --git a/languages/golang/encrypt/query_test.go b/languages/golang/encrypt/query_test.go index b2dc90dc0..e369ff209 100644 --- a/languages/golang/encrypt/query_test.go +++ b/languages/golang/encrypt/query_test.go @@ -304,6 +304,20 @@ func TestTargetLimitIsTheRustFixtures(t *testing.T) { } } +// An unbounded limit pages, and a page past the largest LIMIT PostgreSQL +// takes is cut to it rather than bound as a value database/sql refuses. +func TestAnUnboundedLimitCutsThePageToTheLargestSQLLimit(t *testing.T) { + var unbounded encrypt.Limit + if got := unbounded.FetchOr(50); got != 50 { + t.Fatalf("FetchOr(50) = %d, want 50", got) + } + for _, page := range []uint64{math.MaxInt64, math.MaxInt64 + 1, math.MaxUint64} { + if got := unbounded.FetchOr(page); got != math.MaxInt64 { + t.Fatalf("FetchOr(%d) = %d, want %d", page, got, uint64(math.MaxInt64)) + } + } +} + // A query the field does not answer is refused as a mistyped value is: an // encoding error that names the field and the rule. The typed path's own // refusals are term errors. diff --git a/languages/golang/encrypt/step.go b/languages/golang/encrypt/step.go index 19d970a89..34f93350d 100644 --- a/languages/golang/encrypt/step.go +++ b/languages/golang/encrypt/step.go @@ -143,7 +143,12 @@ func decodeRequest(item any) (ioRequest, bool) { return ioRequest{}, false } } - return r, r.method != "" && r.url != "" + if r.method == "" || r.url == "" { + wipe(r.headers) + wipe(r.body) + return ioRequest{}, false + } + return r, true } // performStep performs every request of a step, in order, and encodes the diff --git a/languages/golang/encrypt/step_test.go b/languages/golang/encrypt/step_test.go index f9977d04f..6abd5bf1e 100644 --- a/languages/golang/encrypt/step_test.go +++ b/languages/golang/encrypt/step_test.go @@ -18,6 +18,7 @@ import ( "github.com/cipherstash/stack/languages/golang/internal/guest" "github.com/cipherstash/vitaminc/bindings/go/vcffi" + "github.com/cipherstash/vitaminc/bindings/go/vcvalue" ) // The step loop, driven hermetically: the guest's init pauses on its @@ -303,6 +304,31 @@ func TestAMalformedStepIsRefused(t *testing.T) { } } +// A request with no method or no URL is refused, and the bearer token and +// body it carried are wiped: the step never holds it, so its wipe cannot. +func TestARefusedRequestIsWiped(t *testing.T) { + for _, missing := range []string{"method", "url"} { + headers := []byte("authorization: Bearer stub-token") + body := []byte("wrapped key") + var obj vcvalue.Object + if missing != "method" { + obj = append(obj, vcvalue.Field{Key: "method", Value: "POST"}) + } + if missing != "url" { + obj = append(obj, vcvalue.Field{Key: "url", Value: "https://zerokms.example/keys"}) + } + obj = append(obj, vcvalue.Field{Key: "headers", Value: headers}, vcvalue.Field{Key: "body", Value: body}) + if _, ok := decodeRequest(obj); ok { + t.Fatalf("a request with no %s was accepted", missing) + } + for name, b := range map[string][]byte{"headers": headers, "body": body} { + if !bytes.Equal(b, make([]byte, len(b))) { + t.Errorf("a request with no %s left its %s unwiped: %q", missing, name, b) + } + } + } +} + func marshalStep(t *testing.T, v any) []byte { t.Helper() b, err := vcffi.Marshal(v) diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index 4c4a7b39b..ec934ef32 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -4235,7 +4235,7 @@ dependencies = [ [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "base64", @@ -4247,7 +4247,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sha2 0.10.9", - "stack-profile 0.43.0", + "stack-profile 0.44.0", "thiserror 1.0.69", "tokio", "tracing", @@ -4304,7 +4304,7 @@ dependencies = [ [[package]] name = "stack-kms" -version = "0.1.0" +version = "0.2.0" dependencies = [ "base16ct", "base64ct", @@ -4318,8 +4318,8 @@ dependencies = [ "serde_json", "serdect", "sha2 0.10.9", - "stack-auth 0.43.0", - "stack-profile 0.43.0", + "stack-auth 0.44.0", + "stack-profile 0.44.0", "thiserror 1.0.69", "tracing", "url", @@ -4346,7 +4346,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/packages/eql/crates/eql-codegen/templates/query_types.sql.j2 b/packages/eql/crates/eql-codegen/templates/query_types.sql.j2 index 8187f10f7..8b1fa94fe 100644 --- a/packages/eql/crates/eql-codegen/templates/query_types.sql.j2 +++ b/packages/eql/crates/eql-codegen/templates/query_types.sql.j2 @@ -60,7 +60,9 @@ BEGIN {%- endfor %} {{ v4_end }} {%- endif %} - AND VALUE->>'v' = '{{ eql_version }}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{ eql_version }}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/crates/eql-codegen/templates/range_functions.sql.j2 b/packages/eql/crates/eql-codegen/templates/range_functions.sql.j2 index 1754139b6..c11eed2b8 100644 --- a/packages/eql/crates/eql-codegen/templates/range_functions.sql.j2 +++ b/packages/eql/crates/eql-codegen/templates/range_functions.sql.j2 @@ -29,7 +29,7 @@ AS $$ SELECT {{ a.body }} $$; --! --! True when `{{ internal_schema }}.lo(r) <= {{ schema }}.{{ b.extractor }}(a) <= {{ internal_schema }}.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{ schema }}.between(col, $1::{{ schema }}.{{ b.range }})`. --! --! @param a {{ b.column }} Encrypted column value diff --git a/packages/eql/crates/eql-codegen/templates/range_types.sql.j2 b/packages/eql/crates/eql-codegen/templates/range_types.sql.j2 index 4a4b87045..970255357 100644 --- a/packages/eql/crates/eql-codegen/templates/range_types.sql.j2 +++ b/packages/eql/crates/eql-codegen/templates/range_types.sql.j2 @@ -9,8 +9,8 @@ --! `{"v": {{ eql_version }}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{ schema }}.query_{{ family_name }}_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/crates/eql-codegen/templates/types.sql.j2 b/packages/eql/crates/eql-codegen/templates/types.sql.j2 index 490920fb0..f45b075fd 100644 --- a/packages/eql/crates/eql-codegen/templates/types.sql.j2 +++ b/packages/eql/crates/eql-codegen/templates/types.sql.j2 @@ -34,7 +34,9 @@ BEGIN {%- endfor %} {{ v4_end }} {%- endif %} - AND VALUE->>'v' = '{{ eql_version }}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{ eql_version }}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/bigint/bigint_types.sql b/packages/eql/src/v3/scalars/bigint/bigint_types.sql index 891f9970e..c3c275340 100644 --- a/packages/eql/src/v3/scalars/bigint/bigint_types.sql +++ b/packages/eql/src/v3/scalars/bigint/bigint_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/bigint/query_bigint_range_functions.sql b/packages/eql/src/v3/scalars/bigint/query_bigint_range_functions.sql index 418810bc8..712b5e9ae 100644 --- a/packages/eql/src/v3/scalars/bigint/query_bigint_range_functions.sql +++ b/packages/eql/src/v3/scalars/bigint/query_bigint_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_bigint_range_ore)`. --! --! @param a public.{{prefix}}_bigint_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_bigint_range_ope)`. --! --! @param a public.{{prefix}}_bigint_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_bigint_range_ope)`. --! --! @param a public.{{prefix}}_bigint_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/bigint/query_bigint_range_types.sql b/packages/eql/src/v3/scalars/bigint/query_bigint_range_types.sql index 1a575330d..d514c280e 100644 --- a/packages/eql/src/v3/scalars/bigint/query_bigint_range_types.sql +++ b/packages/eql/src/v3/scalars/bigint/query_bigint_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_bigint_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/bigint/query_bigint_types.sql b/packages/eql/src/v3/scalars/bigint/query_bigint_types.sql index 283efd8a3..1067400f2 100644 --- a/packages/eql/src/v3/scalars/bigint/query_bigint_types.sql +++ b/packages/eql/src/v3/scalars/bigint/query_bigint_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/boolean/boolean_types.sql b/packages/eql/src/v3/scalars/boolean/boolean_types.sql index 217adc946..101301871 100644 --- a/packages/eql/src/v3/scalars/boolean/boolean_types.sql +++ b/packages/eql/src/v3/scalars/boolean/boolean_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/date/date_types.sql b/packages/eql/src/v3/scalars/date/date_types.sql index d103b52dc..04851a908 100644 --- a/packages/eql/src/v3/scalars/date/date_types.sql +++ b/packages/eql/src/v3/scalars/date/date_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/date/query_date_range_functions.sql b/packages/eql/src/v3/scalars/date/query_date_range_functions.sql index 86a512a43..b1e45cd41 100644 --- a/packages/eql/src/v3/scalars/date/query_date_range_functions.sql +++ b/packages/eql/src/v3/scalars/date/query_date_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_date_range_ore)`. --! --! @param a public.{{prefix}}_date_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_date_range_ope)`. --! --! @param a public.{{prefix}}_date_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_date_range_ope)`. --! --! @param a public.{{prefix}}_date_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/date/query_date_range_types.sql b/packages/eql/src/v3/scalars/date/query_date_range_types.sql index a54bac820..af564340a 100644 --- a/packages/eql/src/v3/scalars/date/query_date_range_types.sql +++ b/packages/eql/src/v3/scalars/date/query_date_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_date_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/date/query_date_types.sql b/packages/eql/src/v3/scalars/date/query_date_types.sql index df73b1936..6c318fc00 100644 --- a/packages/eql/src/v3/scalars/date/query_date_types.sql +++ b/packages/eql/src/v3/scalars/date/query_date_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/double/double_types.sql b/packages/eql/src/v3/scalars/double/double_types.sql index 9065091f0..b9779d8c5 100644 --- a/packages/eql/src/v3/scalars/double/double_types.sql +++ b/packages/eql/src/v3/scalars/double/double_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/double/query_double_range_functions.sql b/packages/eql/src/v3/scalars/double/query_double_range_functions.sql index d5282f6b2..a14662ab5 100644 --- a/packages/eql/src/v3/scalars/double/query_double_range_functions.sql +++ b/packages/eql/src/v3/scalars/double/query_double_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_double_range_ore)`. --! --! @param a public.{{prefix}}_double_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_double_range_ope)`. --! --! @param a public.{{prefix}}_double_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_double_range_ope)`. --! --! @param a public.{{prefix}}_double_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/double/query_double_range_types.sql b/packages/eql/src/v3/scalars/double/query_double_range_types.sql index 5fe72c9ff..098db4396 100644 --- a/packages/eql/src/v3/scalars/double/query_double_range_types.sql +++ b/packages/eql/src/v3/scalars/double/query_double_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_double_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/double/query_double_types.sql b/packages/eql/src/v3/scalars/double/query_double_types.sql index a30e90d79..2ca1b3a36 100644 --- a/packages/eql/src/v3/scalars/double/query_double_types.sql +++ b/packages/eql/src/v3/scalars/double/query_double_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/integer/integer_types.sql b/packages/eql/src/v3/scalars/integer/integer_types.sql index 4f5c541b3..b8969ad81 100644 --- a/packages/eql/src/v3/scalars/integer/integer_types.sql +++ b/packages/eql/src/v3/scalars/integer/integer_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/integer/query_integer_range_functions.sql b/packages/eql/src/v3/scalars/integer/query_integer_range_functions.sql index 68924936e..9b05920bd 100644 --- a/packages/eql/src/v3/scalars/integer/query_integer_range_functions.sql +++ b/packages/eql/src/v3/scalars/integer/query_integer_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_integer_range_ore)`. --! --! @param a public.{{prefix}}_integer_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_integer_range_ope)`. --! --! @param a public.{{prefix}}_integer_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_integer_range_ope)`. --! --! @param a public.{{prefix}}_integer_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/integer/query_integer_range_types.sql b/packages/eql/src/v3/scalars/integer/query_integer_range_types.sql index 9ab117d33..bf9783a9b 100644 --- a/packages/eql/src/v3/scalars/integer/query_integer_range_types.sql +++ b/packages/eql/src/v3/scalars/integer/query_integer_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_integer_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/integer/query_integer_types.sql b/packages/eql/src/v3/scalars/integer/query_integer_types.sql index 6f6638d53..550282e61 100644 --- a/packages/eql/src/v3/scalars/integer/query_integer_types.sql +++ b/packages/eql/src/v3/scalars/integer/query_integer_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/json/json_types.sql b/packages/eql/src/v3/scalars/json/json_types.sql index 047dc2091..d8474e8e2 100644 --- a/packages/eql/src/v3/scalars/json/json_types.sql +++ b/packages/eql/src/v3/scalars/json/json_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/numeric/numeric_types.sql b/packages/eql/src/v3/scalars/numeric/numeric_types.sql index f71b1a75c..2e2f0ce95 100644 --- a/packages/eql/src/v3/scalars/numeric/numeric_types.sql +++ b/packages/eql/src/v3/scalars/numeric/numeric_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/numeric/query_numeric_range_functions.sql b/packages/eql/src/v3/scalars/numeric/query_numeric_range_functions.sql index 60ac2d189..6929ccff7 100644 --- a/packages/eql/src/v3/scalars/numeric/query_numeric_range_functions.sql +++ b/packages/eql/src/v3/scalars/numeric/query_numeric_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_numeric_range_ore)`. --! --! @param a public.{{prefix}}_numeric_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_numeric_range_ope)`. --! --! @param a public.{{prefix}}_numeric_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_numeric_range_ope)`. --! --! @param a public.{{prefix}}_numeric_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/numeric/query_numeric_range_types.sql b/packages/eql/src/v3/scalars/numeric/query_numeric_range_types.sql index f1b35c592..1af4d60bb 100644 --- a/packages/eql/src/v3/scalars/numeric/query_numeric_range_types.sql +++ b/packages/eql/src/v3/scalars/numeric/query_numeric_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_numeric_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/numeric/query_numeric_types.sql b/packages/eql/src/v3/scalars/numeric/query_numeric_types.sql index 8f4ebfde7..166da947e 100644 --- a/packages/eql/src/v3/scalars/numeric/query_numeric_types.sql +++ b/packages/eql/src/v3/scalars/numeric/query_numeric_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/real/query_real_range_functions.sql b/packages/eql/src/v3/scalars/real/query_real_range_functions.sql index c1a74e2ca..4e0e3e626 100644 --- a/packages/eql/src/v3/scalars/real/query_real_range_functions.sql +++ b/packages/eql/src/v3/scalars/real/query_real_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_real_range_ore)`. --! --! @param a public.{{prefix}}_real_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_real_range_ope)`. --! --! @param a public.{{prefix}}_real_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_real_range_ope)`. --! --! @param a public.{{prefix}}_real_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/real/query_real_range_types.sql b/packages/eql/src/v3/scalars/real/query_real_range_types.sql index c71092354..09b656ea4 100644 --- a/packages/eql/src/v3/scalars/real/query_real_range_types.sql +++ b/packages/eql/src/v3/scalars/real/query_real_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_real_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/real/query_real_types.sql b/packages/eql/src/v3/scalars/real/query_real_types.sql index 6198f2279..a85dad7d5 100644 --- a/packages/eql/src/v3/scalars/real/query_real_types.sql +++ b/packages/eql/src/v3/scalars/real/query_real_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/real/real_types.sql b/packages/eql/src/v3/scalars/real/real_types.sql index cf7804519..0ed3899a2 100644 --- a/packages/eql/src/v3/scalars/real/real_types.sql +++ b/packages/eql/src/v3/scalars/real/real_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/smallint/query_smallint_range_functions.sql b/packages/eql/src/v3/scalars/smallint/query_smallint_range_functions.sql index df1dffbe9..3ac62a2f5 100644 --- a/packages/eql/src/v3/scalars/smallint/query_smallint_range_functions.sql +++ b/packages/eql/src/v3/scalars/smallint/query_smallint_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_smallint_range_ore)`. --! --! @param a public.{{prefix}}_smallint_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_smallint_range_ope)`. --! --! @param a public.{{prefix}}_smallint_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_smallint_range_ope)`. --! --! @param a public.{{prefix}}_smallint_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/smallint/query_smallint_range_types.sql b/packages/eql/src/v3/scalars/smallint/query_smallint_range_types.sql index 1d167b904..24d51387c 100644 --- a/packages/eql/src/v3/scalars/smallint/query_smallint_range_types.sql +++ b/packages/eql/src/v3/scalars/smallint/query_smallint_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_smallint_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/smallint/query_smallint_types.sql b/packages/eql/src/v3/scalars/smallint/query_smallint_types.sql index e52403c68..f4be4a98e 100644 --- a/packages/eql/src/v3/scalars/smallint/query_smallint_types.sql +++ b/packages/eql/src/v3/scalars/smallint/query_smallint_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/smallint/smallint_types.sql b/packages/eql/src/v3/scalars/smallint/smallint_types.sql index 103a397c4..b9335705f 100644 --- a/packages/eql/src/v3/scalars/smallint/smallint_types.sql +++ b/packages/eql/src/v3/scalars/smallint/smallint_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/text/query_text_range_functions.sql b/packages/eql/src/v3/scalars/text/query_text_range_functions.sql index 02c5b9a9b..bf59c5822 100644 --- a/packages/eql/src/v3/scalars/text/query_text_range_functions.sql +++ b/packages/eql/src/v3/scalars/text/query_text_range_functions.sql @@ -87,7 +87,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_text_range_ore)`. --! --! @param a public.{{prefix}}_text_ord_ore Encrypted column value @@ -122,7 +122,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_text_range_ore)`. --! --! @param a public.{{prefix}}_text_search_ore Encrypted column value @@ -157,7 +157,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_text_range_ope)`. --! --! @param a public.{{prefix}}_text_ord Encrypted column value @@ -187,7 +187,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_text_range_ope)`. --! --! @param a public.{{prefix}}_text_ord_ope Encrypted column value @@ -217,7 +217,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_text_range_ope)`. --! --! @param a public.{{prefix}}_text_search Encrypted column value diff --git a/packages/eql/src/v3/scalars/text/query_text_range_types.sql b/packages/eql/src/v3/scalars/text/query_text_range_types.sql index 0627fd499..7add96f04 100644 --- a/packages/eql/src/v3/scalars/text/query_text_range_types.sql +++ b/packages/eql/src/v3/scalars/text/query_text_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_text_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/text/query_text_types.sql b/packages/eql/src/v3/scalars/text/query_text_types.sql index 2ae6b8aa8..f2a5f602d 100644 --- a/packages/eql/src/v3/scalars/text/query_text_types.sql +++ b/packages/eql/src/v3/scalars/text/query_text_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -44,7 +46,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'bf' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -71,7 +75,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -94,7 +100,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -117,7 +125,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -150,7 +160,9 @@ BEGIN AND (NOT (VALUE ? 'ob') OR (jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203'))) -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -179,7 +191,9 @@ BEGIN AND (NOT (VALUE ? 'op') OR (jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01'))) -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/text/text_types.sql b/packages/eql/src/v3/scalars/text/text_types.sql index 65c5c216b..3fb643c95 100644 --- a/packages/eql/src/v3/scalars/text/text_types.sql +++ b/packages/eql/src/v3/scalars/text/text_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -53,7 +57,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'bf' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -80,7 +86,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -103,7 +111,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -126,7 +136,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -154,7 +166,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -178,7 +192,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_functions.sql b/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_functions.sql index cab511249..c554f792d 100644 --- a/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_functions.sql +++ b/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_functions.sql @@ -85,7 +85,7 @@ AS $$ SELECT {{prefix}}_internal.ope_cllw(jsonb_build_object('op', r::jsonb -> ' --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term_ore(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_timestamp_range_ore)`. --! --! @param a public.{{prefix}}_timestamp_ord_ore Encrypted column value @@ -120,7 +120,7 @@ AS $$ SELECT {{prefix}}.ord_term_ore(a) >= {{prefix}}_internal.lo(r) AND {{prefi --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_timestamp_range_ope)`. --! --! @param a public.{{prefix}}_timestamp_ord Encrypted column value @@ -150,7 +150,7 @@ AS $$ SELECT {{prefix}}.ord_term(a) >= {{prefix}}_internal.lo(r) AND {{prefix}}. --! --! True when `{{prefix}}_internal.lo(r) <= {{prefix}}.ord_term(a) <= {{prefix}}_internal.hi(r)`. --! The range is one operand, so a statement cannot pair the low bound of one ---! query with the high bound of another (ADR-0009, decision 7): +--! query with the high bound of another (Stack Encrypt ADR-0009, decision 7): --! `WHERE {{prefix}}.between(col, $1::{{prefix}}.query_timestamp_range_ope)`. --! --! @param a public.{{prefix}}_timestamp_ord_ope Encrypted column value diff --git a/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_types.sql b/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_types.sql index e58f726c2..90707ec3d 100644 --- a/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_types.sql +++ b/packages/eql/src/v3/scalars/timestamp/query_timestamp_range_types.sql @@ -9,8 +9,8 @@ --! `{"v": {{eql_version}}, "i": {...}, "lo": , "hi": }`. Each bound is the --! single-bound query domain's `ob` (ORE) or `op` (OPE) value, in a --! `{v, i, lo, hi}` envelope. Both bounds are included. There is one ---! range domain per ordering term the family stores (ADR-0009, ---! decisions 7 and 8). +--! range domain per ordering term the family stores (Stack Encrypt +--! ADR-0009, decisions 7 and 8). --! @note Cast the range parameter to its exact range domain --! (`$1::{{prefix}}.query_timestamp_range_ore`). An uncast `$1`, a `jsonb` --! parameter, or a range of another term or family fails with 42725 diff --git a/packages/eql/src/v3/scalars/timestamp/query_timestamp_types.sql b/packages/eql/src/v3/scalars/timestamp/query_timestamp_types.sql index 23a1bbc4d..894f68275 100644 --- a/packages/eql/src/v3/scalars/timestamp/query_timestamp_types.sql +++ b/packages/eql/src/v3/scalars/timestamp/query_timestamp_types.sql @@ -26,7 +26,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'hm' AND NOT (VALUE ? 'c') - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -52,7 +54,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -74,7 +78,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -96,7 +102,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/src/v3/scalars/timestamp/timestamp_types.sql b/packages/eql/src/v3/scalars/timestamp/timestamp_types.sql index d0bd59e57..6852d7b52 100644 --- a/packages/eql/src/v3/scalars/timestamp/timestamp_types.sql +++ b/packages/eql/src/v3/scalars/timestamp/timestamp_types.sql @@ -17,7 +17,9 @@ BEGIN AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -35,7 +37,9 @@ BEGIN AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -61,7 +65,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'ob') = 'string' AND left(VALUE ->> 'ob', 4) IN ('0202', '0203') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -83,7 +89,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; @@ -105,7 +113,9 @@ BEGIN AND jsonb_typeof(VALUE -> 'op') = 'string' AND left(VALUE ->> 'op', 2) IN ('00', '01') -- {{/v4}} - AND VALUE->>'v' = '{{eql_version}}' + -- `"v": null` makes the comparison NULL, and a CHECK that is NULL + -- passes, so COALESCE refuses it. A NULL value still passes. + AND COALESCE(VALUE->>'v' = '{{eql_version}}', VALUE IS NULL) ); END IF; diff --git a/packages/eql/tests/sqlx/tests/v3_v4_coexistence_tests.rs b/packages/eql/tests/sqlx/tests/v3_v4_coexistence_tests.rs index d92d9a1e5..21cfc97e2 100644 --- a/packages/eql/tests/sqlx/tests/v3_v4_coexistence_tests.rs +++ b/packages/eql/tests/sqlx/tests/v3_v4_coexistence_tests.rs @@ -227,17 +227,24 @@ async fn each_version_matches_its_own_terms(pool: PgPool) -> Result<()> { async fn wrong_version_insert_fails_the_domain_check(pool: PgPool) -> Result<()> { install_v4(&pool).await?; - for (domain, version) in [ - ("public.eql_v3_integer_eq", 4), - ("public.eql_v4_integer_eq", 3), + // A query domain holds no ciphertext, so its payload drops `c`. + for (domain, payload, version) in [ + ("public.eql_v3_integer_eq", "payload", "4"), + ("public.eql_v4_integer_eq", "payload", "3"), + // A null version is neither: the CHECK must refuse it, not pass it + // as a NULL comparison. + ("public.eql_v3_integer_eq", "payload", "null"), + ("public.eql_v4_integer_eq", "payload", "null"), + ("eql_v3.query_integer_eq", "payload - 'c'", "null"), + ("eql_v4.query_integer_eq", "payload - 'c'", "null"), ] { let err = sqlx::query(&format!( - "SELECT jsonb_set(payload, '{{v}}', '{version}')::{domain} \ + "SELECT jsonb_set({payload}, '{{v}}', '{version}')::{domain} \ FROM fixtures.eql_v3_integer WHERE plaintext = 42" )) .execute(&pool) .await - .expect_err("a payload of the other version must not fit the domain"); + .expect_err("a payload of another version must not fit the domain"); assert_eq!( sqlstate(&err).as_deref(), Some(CHECK_VIOLATION), diff --git a/packages/stack-auth/CHANGELOG.md b/packages/stack-auth/CHANGELOG.md index 8cd4ec3f5..dc5755c53 100644 --- a/packages/stack-auth/CHANGELOG.md +++ b/packages/stack-auth/CHANGELOG.md @@ -1,4 +1,39 @@ +## [0.44.0] - 2026-10-27 + +### ⚠ Breaking changes + +- `OidcFederationStrategy` keeps one CTS token per provider JWT. It asks the + `OidcProvider` for the caller's JWT on every `get_token` and keeps one + refresh engine per distinct JWT in a bounded least-recently-used cache + (1024 entries by default, `cache_capacity` on the builder). It used to + serve one cached token to every caller until it expired, so on a server + serving many users one user's requests could go out under another's token. +- Requires stack-profile 0.44. + +### Features + +- `OidcFederationStrategy::get_token_for_jwt`, the exchange for a JWT the + caller already holds. `get_token` is the provider fetch followed by it, and + both share one cache. +- Every error carries a path-style miette code (`stack_auth::invalid_crn`) + beside its unchanged frozen code, and implements stack-profile's + `ErrorPayload`. `ERROR_CODES` lists every code. Re-exports `diagnostic` and + `ErrorPayload`. +- `tls::client_config`, the rustls client configuration the crate's HTTP + clients use. + +### Fixes + +- TLS chains carrying the compromised eMudhra intermediate CA keys are + refused as revoked, including their Sectigo cross-signs. The bundled client + fails closed when that configuration cannot be built. +- A refused exchange's `ServerError` names the status and, for the auth + server's JSON error, its `error_description`, never the response body. +- `AuthError::Store` keeps its cause as its `source()`, and a build without + `http` whose strategy has no transport says so + (`stack_auth::no_transport`). + ## [0.43.0] - 2026-10-02 diff --git a/packages/stack-auth/Cargo.toml b/packages/stack-auth/Cargo.toml index 3fae08b09..095e22d57 100644 --- a/packages/stack-auth/Cargo.toml +++ b/packages/stack-auth/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stack-auth" description = "Authentication library for CipherStash services" -version = "0.43.0" +version = "0.44.0" edition.workspace = true authors.workspace = true repository.workspace = true diff --git a/packages/stack-auth/fuzz/Cargo.lock b/packages/stack-auth/fuzz/Cargo.lock index d08157f02..13914968d 100644 --- a/packages/stack-auth/fuzz/Cargo.lock +++ b/packages/stack-auth/fuzz/Cargo.lock @@ -2594,7 +2594,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "base64", @@ -2634,7 +2634,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 6155397d3..8f4f4c4e7 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.3.0] - 2026-10-27 ### Breaking @@ -506,6 +506,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 carry byte is not 0 or 1, which clww-ore's comparator already refuses (`TermBytesError::MalformedCllwCiphertext`). +- **stack-encrypt requires stack-kms 0.2 and stack-auth 0.44.** It + re-exports `stack_kms::{diagnostic, ErrorPayload}` and stack-kms as + `stack_encrypt::kms`, so a crate that names a stack-kms or stack-auth type + stack-encrypt takes or returns moves to those versions with it. - **stack-encrypt requires the vitaminc 0.8 crates, clww-ore 0.6 and ore-rs 0.10.** A crate that names a vitaminc type stack-encrypt takes or returns moves to 0.8 with it. What changes for a caller: diff --git a/packages/stack-encrypt/Cargo.toml b/packages/stack-encrypt/Cargo.toml index b56af7ea2..88aacc8dc 100644 --- a/packages/stack-encrypt/Cargo.toml +++ b/packages/stack-encrypt/Cargo.toml @@ -17,14 +17,14 @@ license-file = "LICENSE" # `StackCipher::new()` fall back to the client key `stash auth login` writes # to the profile directory (`secretkey.json`) when CS_CLIENT_ID / # CS_CLIENT_KEY are not set — the same place `AutoStrategy` finds the token. -stack-kms = { path = "../stack-kms", version = "0.1.0", default-features = false, features = ["profile"] } +stack-kms = { path = "../stack-kms", version = "0.2.0", default-features = false, features = ["profile"] } # `StackCipher::new()` builds a ZeroKMS client from the environment, so its # return type names the auto-detected auth strategy. Only needed with `http`. stack-auth = { workspace = true, optional = true } # `#[derive(EncryptFrom)]` / `#[derive(DecryptInto)]`, re-exported from `target`. stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.3.0" } -# The workspace vitaminc (0.6.0): one canonical context encoding shared by +# The workspace vitaminc: one canonical context encoding shared by # the AEAD and PRF derivations (`IntoContext`, cipherstash/vitaminc#339), # `NonEmpty::with`, `From for NonEmpty` (#314) and the parts view # (#318), all of which this crate relies on. All five must share one diff --git a/packages/stack-encrypt/fuzz/Cargo.lock b/packages/stack-encrypt/fuzz/Cargo.lock index 1fcd38200..7d733cc3b 100644 --- a/packages/stack-encrypt/fuzz/Cargo.lock +++ b/packages/stack-encrypt/fuzz/Cargo.lock @@ -2048,7 +2048,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "base64", @@ -2128,7 +2128,7 @@ dependencies = [ [[package]] name = "stack-kms" -version = "0.1.0" +version = "0.2.0" dependencies = [ "base16ct", "base64ct", @@ -2156,7 +2156,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/packages/stack-guest-abi/src/call.rs b/packages/stack-guest-abi/src/call.rs index 9ed9f9e78..4c1912353 100644 --- a/packages/stack-guest-abi/src/call.rs +++ b/packages/stack-guest-abi/src/call.rs @@ -99,6 +99,7 @@ pub fn take_last_error() -> Option> { mod tests { use super::*; use crate::status::{STATUS_ENCODING, STATUS_INTERNAL}; + use crate::value::HostValue; /// A paused operation is not a failure: it records no error, and clears /// one recorded on the way to the pause. @@ -111,7 +112,6 @@ mod tests { assert_eq!(paused, Err(STATUS_NEEDS_IO)); assert!(!last_error::is_set()); } - use crate::value::HostValue; /// A packed result, read the way the Go host reads it. #[derive(Debug, PartialEq)] diff --git a/packages/stack-kms/CHANGELOG.md b/packages/stack-kms/CHANGELOG.md new file mode 100644 index 000000000..1ae506914 --- /dev/null +++ b/packages/stack-kms/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2026-10-27 + +### Breaking + +- Requires stack-auth 0.44 and stack-profile 0.44. + +### Features + +- Every error carries a `stack_kms::*` miette code (listed in + `ERROR_CODES`), help, and structured fields through the shared + `ErrorPayload`. Re-exports `diagnostic` and `ErrorPayload`. +- `DeterministicSource` (behind `test-support`), a key source whose keys are + a function of a seed and the descriptor, for fixtures that hold real sealed + bytes. It refuses a wrong context as ZeroKMS does, as `Forbidden`. + +### Fixes + +- The HTTP client refuses TLS chains carrying the compromised eMudhra CA + keys, through stack-auth's `tls::client_config`. + +## [0.1.0] - 2026-10-04 + +- First release. diff --git a/packages/stack-kms/Cargo.toml b/packages/stack-kms/Cargo.toml index f1687fd49..4a189ac7d 100644 --- a/packages/stack-kms/Cargo.toml +++ b/packages/stack-kms/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stack-kms" description = "Standalone client for ZeroKMS key generation and retrieval" -version = "0.1.0" +version = "0.2.0" edition.workspace = true authors.workspace = true repository.workspace = true diff --git a/packages/stack-kms/fuzz/Cargo.lock b/packages/stack-kms/fuzz/Cargo.lock index c7e4cc7fd..37ba283af 100644 --- a/packages/stack-kms/fuzz/Cargo.lock +++ b/packages/stack-kms/fuzz/Cargo.lock @@ -1937,7 +1937,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.43.0" +version = "0.44.0" dependencies = [ "aquamarine", "base64", @@ -1964,7 +1964,7 @@ dependencies = [ [[package]] name = "stack-kms" -version = "0.1.0" +version = "0.2.0" dependencies = [ "base16ct", "base64ct", @@ -2000,7 +2000,7 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.43.0" +version = "0.44.0" dependencies = [ "dirs", "gethostname", diff --git a/packages/stack-profile/CHANGELOG.md b/packages/stack-profile/CHANGELOG.md index ee9d741ea..a36016fd8 100644 --- a/packages/stack-profile/CHANGELOG.md +++ b/packages/stack-profile/CHANGELOG.md @@ -37,6 +37,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.44.0] - 2026-10-27 + +### Features + +- `ErrorPayload`, a `miette::Diagnostic` that gives an error's structured + fields, and the `diagnostic` module with the rule for what an error may + contain (no plaintext, key material, tokens or ciphertext). stack-auth, + stack-kms and stack-encrypt share them. +- `ProfileError` carries a `stack_profile::*` miette code and help on every + variant. + +### Fixes + +- `NoCurrentWorkspace` and `WorkspaceNotFound` name `stash auth login` in + their help; the message named commands the CLI does not have. + ## [0.43.0] - 2026-10-02 diff --git a/packages/stack-profile/Cargo.toml b/packages/stack-profile/Cargo.toml index b2b57354e..dcd9f73e5 100644 --- a/packages/stack-profile/Cargo.toml +++ b/packages/stack-profile/Cargo.toml @@ -2,7 +2,7 @@ name = "stack-profile" description = "Centralised ~/.cipherstash profile file management" license-file = "LICENSE" -version = "0.43.0" +version = "0.44.0" edition.workspace = true authors.workspace = true repository.workspace = true