diff --git a/.gitignore b/.gitignore index d9a13a2e..7c1b67f2 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,10 @@ web_modules/ .env.local .envrc +# Local mise overrides (CipherStash credentials, etc.) +mise.local.toml +.mise.local.toml + # parcel-bundler cache (https://parceljs.org/) .parcel-cache diff --git a/docs/superpowers/specs/2026-05-12-encrypted-domain-types-design.md b/docs/superpowers/specs/2026-05-12-encrypted-domain-types-design.md new file mode 100644 index 00000000..914f8577 --- /dev/null +++ b/docs/superpowers/specs/2026-05-12-encrypted-domain-types-design.md @@ -0,0 +1,195 @@ +# High-Level Encrypted Domain Types Prototype Design + +## Context + +EQL currently exposes one public encrypted column type, `public.eql_v2_encrypted`, +implemented as a composite type with a single `jsonb` payload field. Query behavior +is selected dynamically from the encrypted payload terms that are present (`hm`, +`bf`, `ob`, `opf`, `opv`, `sv`, etc.). + +The new goal is to add high-level SQL column types such as `encrypted_text`, +`encrypted_jsonb`, and `encrypted_int4`. These types should make application DDL +clearer and give each plaintext shape a static, predictable SQL operator surface. +They should not rely on the broad dynamic dispatch behavior of +`eql_v2_encrypted`. + +The prototype is intentionally limited to: + +- `public.encrypted_text` +- `public.encrypted_jsonb` +- `public.encrypted_int4` + +Configuration inference, automatic registration, broad type coverage, and +production migration behavior are out of scope for the prototype. The prototype +exists to prove whether `jsonb` domain types can provide a clean client-facing +DDL surface while still producing indexable query plans without operator +classes. + +## History And Spike Findings + +A previous branch tried changing `eql_v2_encrypted` itself from a composite type +to a `jsonb` domain. That PR closed unmerged with failing CI, and there is no +clear written rationale for the failure. Separately, EQL has kept +`public.eql_v2_encrypted` and `public.eql_v2_configuration` outside the +`eql_v2` schema so EQL upgrades can drop and recreate `eql_v2` without +cascading into customer columns. + +A transient SQL spike compared three shapes: + +- domain over raw `jsonb` +- domain over `public.eql_v2_encrypted` +- independent composite type with `(data jsonb)` + +The spike showed that domains over `public.eql_v2_encrypted` are ergonomic and +can use existing helpers, but inherit base EQL operators when exact domain +operators are absent. Independent composites avoid inherited behavior, but need +more casts and exact helper/operator wrappers. + +The approved design is simpler: define the high-level types as domains over +raw `jsonb`, then define exact operators for supported and unsupported +operations. This removes the extra `eql_v2_encrypted` layer from the new public +types. + +## Type Model + +Create public domain types over `jsonb`: + +```sql +CREATE DOMAIN public.encrypted_text AS jsonb; +CREATE DOMAIN public.encrypted_jsonb AS jsonb; +CREATE DOMAIN public.encrypted_int4 AS jsonb; +``` + +The payload remains the existing EQL encrypted JSONB payload. The specific +types do not depend on `public.eql_v2_encrypted` for storage or operator +dispatch. + +Because PostgreSQL domains can fall back to base-type behavior, every public +operation in the supported SQL surface must have an exact domain operator: + +- supported operations delegate to fixed index-term helpers; +- unsupported operations raise a type-specific error. + +This prevents accidental fallback to native `jsonb` semantics for common SQL +operators. + +## Prototype Acceptance Criteria + +The prototype must prove these properties: + +- exact domain operators resolve for supported operations; +- exact blocker operators prevent common unsupported operations from falling + through to native `jsonb` behavior; +- supported hot-path operator functions are inlineable SQL functions with no + `SET search_path` clause; +- bare operator predicates use functional indexes and do not require custom + btree or hash operator classes; +- where existing helper signatures are awkward, temporary typed helper wrappers + are small, `LANGUAGE sql`, immutable, strict, parallel-safe, and inlineable + when used in indexed predicates. + +## Operator Surface + +### `encrypted_text` + +Supported: + +- `=` and `<>`, using the `hm` term through `eql_v2.hmac_256(value::jsonb)` +- `~~` and `~~*`, using the `bf` term through `eql_v2.bloom_filter(value::jsonb)` + +Unsupported blockers: + +- `<`, `<=`, `>`, `>=` +- `@>`, `<@` +- `->`, `->>` + +### `encrypted_int4` + +Supported: + +- `=` and `<>`, using the `hm` term through `eql_v2.hmac_256(value::jsonb)` +- `<`, `<=`, `>`, `>=`, using OPE terms by default through an inlineable + expression over `value::jsonb` + +Unsupported blockers: + +- `~~`, `~~*` +- `@>`, `<@` +- `->`, `->>` + +### `encrypted_jsonb` + +Supported: + +- `=` and `<>`, using the `hm` term through `eql_v2.hmac_256(value::jsonb)` +- `@>` and `<@`, using `sv` through inlineable typed STE vector helpers or + wrappers +- `->` and `->>`, using stubbed or adapted encrypted JSON path helpers for the + domain type + +Unsupported blockers: + +- `<`, `<=`, `>`, `>=` +- `~~`, `~~*` + +## Out Of Scope + +Do not add configuration inference in this prototype. The prototype should not +change `eql_v2.add_column`, `eql_v2.add_search_config`, or the configuration +validation functions. + +Do not add automatic registration or event triggers in this prototype. + +Do not add full support for additional encrypted scalar types in this prototype. +The three selected types are enough to test text, scalar range, and JSONB +operator behavior. + +## Error Handling + +Unsupported exact operators should raise clear errors: + +```text +operator < is not supported for encrypted_text +operator ~~ is not supported for encrypted_int4 +operator -> is not supported for encrypted_int4 +``` + +Missing required encrypted index terms should fail through the fixed helper path +with the existing helper errors, such as missing `hm`, `bf`, `opf`, or `sv`. + +Supported hot-path functions should not raise custom errors for missing terms if +an existing helper already provides a precise missing-term error. + +## Testing + +Add focused SQLx coverage for the first three domain types: + +- Domain creation and assignment from valid encrypted JSONB payloads. +- Supported operators for each type. +- Unsupported operators raise the exact type-specific error instead of falling + through to native `jsonb` behavior. +- Functional indexes engage for supported terms: + - `encrypted_text`: `eql_v2.hmac_256(col::jsonb)`, + `eql_v2.bloom_filter(col::jsonb)` + - `encrypted_int4`: `eql_v2.hmac_256(col::jsonb)`, and an OPE order + expression over `col::jsonb` + - `encrypted_jsonb`: `eql_v2.hmac_256(col::jsonb)`, and a typed STE vector + array helper or overload that accepts `encrypted_jsonb` +- `EXPLAIN` plans show index scans for bare operator predicates such as + `col = rhs`, `col ~~ rhs`, `col < rhs`, and `col @> rhs`. +- The same predicates do not require btree/hash operator classes. +- Prepared statements with domain-typed parameters still resolve to exact + domain operators. + +## Implementation Boundary + +Write the first three type surfaces manually. Do not introduce a generator in +the prototype. Manual SQL keeps the spike easy to audit and +lets tests prove the domain-over-`jsonb` approach before expanding to +`encrypted_int2`, `encrypted_int8`, numeric, floating-point, boolean, date, and +timestamp types. + +Supported operator functions and helper wrappers that appear in indexed +predicates must be SQL-language functions intended for planner inlining. +Unsupported blocker functions can use PL/pgSQL because they are not performance +paths. diff --git a/mise.toml b/mise.toml index fbf499b4..6fd0b7c6 100644 --- a/mise.toml +++ b/mise.toml @@ -14,7 +14,7 @@ "python" = "3.13" [task_config] -includes = ["tasks", "tasks/postgres.toml"] +includes = ["tasks", "tasks/postgres.toml", "tasks/bench.toml", "tasks/fixtures.toml"] [env] POSTGRES_DB = "cipherstash" diff --git a/tasks/bench.toml b/tasks/bench.toml new file mode 100644 index 00000000..72abc487 --- /dev/null +++ b/tasks/bench.toml @@ -0,0 +1,64 @@ +["bench:up"] +description = "Start Postgres + Proxy for benchmark data generation" +dir = "{{config_root}}" +run = """ +if [ ! -f tests/benchmarks/.env ]; then + echo "ERROR: tests/benchmarks/.env missing. Copy .env.example and fill in credentials." >&2 + exit 1 +fi +docker compose --env-file tests/benchmarks/.env -f tests/benchmarks/docker-compose.yml up -d +export PGPASSWORD="password" +echo "Waiting for bench-postgres on localhost:7433..." +for i in $(seq 1 60); do + if psql -U cipherstash -d cipherstash -h localhost -p 7433 -c 'SELECT 1' >/dev/null 2>&1; then + echo "bench-postgres ready." + break + fi + sleep 1 + if [ "$i" -eq 60 ]; then + echo "bench-postgres did not become ready in 60s." + echo + echo '=== bench-postgres logs ===' + docker logs bench-postgres 2>&1 | tail -40 + exit 1 + fi +done + +echo "Waiting for bench-proxy on localhost:6433..." +for i in $(seq 1 60); do + if psql -U cipherstash -d cipherstash -h localhost -p 6433 -c 'SELECT 1' >/dev/null 2>&1; then + echo "bench-proxy ready." + exit 0 + fi + sleep 1 +done +echo "bench-proxy did not become ready in 60s." +echo +echo '=== bench-proxy logs ===' +docker logs bench-proxy 2>&1 | tail -40 +exit 1 +""" + +["bench:down"] +description = "Stop benchmark Postgres + Proxy" +dir = "{{config_root}}" +run = """ +docker compose -f tests/benchmarks/docker-compose.yml down -v +""" + +["bench:generate"] +description = "Generate 100K encrypted bench dataset (requires bench:up first)" +# `build` produces release/cipherstash-encrypt.sql, which generate.sh +# installs into the bench Postgres container before applying schema.sql. +depends = ["build"] +dir = "{{config_root}}" +run = """ +tests/benchmarks/generate.sh 100k +""" + +["bench:full"] +description = "Run committed SQLx bench/regression suite" +dir = "{{config_root}}" +run = """ +mise run --output prefix test:bench +""" diff --git a/tasks/fixtures.toml b/tasks/fixtures.toml new file mode 100644 index 00000000..00279880 --- /dev/null +++ b/tasks/fixtures.toml @@ -0,0 +1,46 @@ +["proxy:up"] +description = "Start CipherStash Proxy connected to existing Postgres" +# Reuses the tests/docker-compose.yml Postgres on POSTGRES_PORT. +# CS_* credentials are read from the shell environment (mise/direnv/profile). +# Readiness is verified from the host (the proxy image lacks busybox nc, so +# the container-internal healthcheck cannot be used). Dumps container logs +# on failure so the user does not need a separate `docker logs` invocation. +dir = "{{config_root}}/tests" +run = """ +docker compose -f docker-compose.proxy.yml up -d +echo "Waiting for proxy on localhost:6432..." +export PGPASSWORD="${POSTGRES_PASSWORD:-password}" +for i in $(seq 1 60); do + if psql -U "${POSTGRES_USER:-cipherstash}" -d "${POSTGRES_DB:-cipherstash}" \ + -h localhost -p 6432 -c 'SELECT 1' >/dev/null 2>&1; then + echo "Proxy ready." + exit 0 + fi + sleep 1 +done +echo "Proxy did not become ready in 60s." +echo +echo '=== cipherstash-proxy logs ===' +docker logs cipherstash-proxy 2>&1 | tail -40 +exit 1 +""" + +["proxy:logs"] +description = "Tail CipherStash Proxy container logs" +dir = "{{config_root}}/tests" +run = "docker logs --tail 100 -f cipherstash-proxy" + +["proxy:down"] +description = "Stop CipherStash Proxy" +dir = "{{config_root}}/tests" +run = "docker compose -f docker-compose.proxy.yml down" + +["fixture:int:generate"] +description = "Generate encrypted_int4 fixture (009) via Proxy" +# Prerequisites: +# - mise run postgres:up (existing Postgres on POSTGRES_PORT) +# - mise run reset (ensures EQL is installed in that Postgres) +# - mise run proxy:up (Proxy on localhost:6432) +depends = ["build"] +dir = "{{config_root}}" +run = "tasks/fixtures/generate_encrypted_int4.sh" diff --git a/tasks/fixtures/_generate_common.sh b/tasks/fixtures/_generate_common.sh new file mode 100644 index 00000000..0d666314 --- /dev/null +++ b/tasks/fixtures/_generate_common.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Common helpers for fixture generators. Sourced — not executed directly. +# Sets PG_URL / PROXY_URL and exposes restart_proxy_and_wait + dump_fixture_table. + +# Resolve Postgres / Proxy connection from mise [env] (POSTGRES_*) with the +# usual defaults. PROXY_PORT comes from tests/docker-compose.proxy.yml. +PG_USER="${POSTGRES_USER:-cipherstash}" +PG_PASSWORD="${POSTGRES_PASSWORD:-password}" +PG_DB="${POSTGRES_DB:-cipherstash}" +PG_HOST="${POSTGRES_HOST:-localhost}" +PG_PORT="${POSTGRES_PORT:-7432}" +PROXY_PORT="${PROXY_PORT:-6432}" + +PG_URL="postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DB}" +PROXY_URL="postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PROXY_PORT}/${PG_DB}" + +export PGPASSWORD="$PG_PASSWORD" + +# Proxy caches its encrypt config at connection-handler init time, so any +# add_search_config call applied AFTER Proxy started won't take effect +# until Proxy reconnects. Restart and wait for it to come back. +restart_proxy_and_wait() { + echo "==> Restarting Proxy so it reloads the new encrypt config" + docker restart cipherstash-proxy >/dev/null + + for i in $(seq 1 60); do + if psql "$PROXY_URL" -c 'SELECT 1' >/dev/null 2>&1; then + echo " Proxy ready." + return 0 + fi + sleep 1 + done + + echo "ERROR: Proxy did not come back up after restart" >&2 + docker logs cipherstash-proxy 2>&1 | tail -20 + return 1 +} + +# Render fixture rows as INSERT statements using format(%L). Caller supplies: +# $1 = source table name (e.g. bench_text) +# $2 = destination table name in the migration (e.g. encrypted_text_plaintext) +# $3 = comma-separated source-column projection +# (e.g. "id, plaintext, (encrypted_text).data::text") +# $4 = comma-separated destination column types for format() placeholders +# (e.g. "%L, %L, %L::jsonb") +# $5 = destination column-name tuple +# (e.g. "(id, plaintext, payload)") +# $6 = output path +# +# The migration is written with a DROP / CREATE preamble plus the rendered +# INSERT statements. The CREATE statement must be supplied by the caller via +# stdin BEFORE calling this function; see how each generator pipes it in. +dump_fixture_table() { + local src_table="$1" + local dst_table="$2" + local src_projection="$3" + local fmt_placeholders="$4" + local dst_columns="$5" + local output_path="$6" + + psql "$PG_URL" -v ON_ERROR_STOP=1 -t -A -c " +SELECT format( + 'INSERT INTO ${dst_table} ${dst_columns} VALUES (${fmt_placeholders});', + ${src_projection} +) +FROM ${src_table} +ORDER BY id; +" >> "$output_path" +} diff --git a/tasks/fixtures/encrypted_int4_schema.sql b/tasks/fixtures/encrypted_int4_schema.sql new file mode 100644 index 00000000..5d870590 --- /dev/null +++ b/tasks/fixtures/encrypted_int4_schema.sql @@ -0,0 +1,30 @@ +-- Schema for the encrypted_int4 plaintext-paired fixture. +-- Applied by tasks/fixtures/generate_encrypted_int4.sh; the generator +-- restarts Proxy afterwards so it reloads the new encrypt config. + +DROP TABLE IF EXISTS bench_int4; + +CREATE TABLE bench_int4 ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + plaintext INTEGER NOT NULL, + encrypted_int4 eql_v2_encrypted +); + +-- Idempotency: drop any prior bench_int4 search-config rows so re-running +-- the generator doesn't error with "unique index exists for column". +SELECT eql_v2.remove_search_config('bench_int4', 'encrypted_int4', 'unique') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench_int4,encrypted_int4,indexes,unique}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench_int4', 'encrypted_int4', 'ore') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench_int4,encrypted_int4,indexes,ore}' IS NOT NULL + ); + +-- unique → HMAC (drives =, <>); ore → OPE bytes (drives <, <=, >, >=). +SELECT eql_v2.add_search_config('bench_int4', 'encrypted_int4', 'unique', 'int'); +SELECT eql_v2.add_search_config('bench_int4', 'encrypted_int4', 'ore', 'int'); diff --git a/tasks/fixtures/generate_encrypted_int4.sh b/tasks/fixtures/generate_encrypted_int4.sh new file mode 100755 index 00000000..5662845a --- /dev/null +++ b/tasks/fixtures/generate_encrypted_int4.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generates an encrypted_int4 fixture by running an integer value set +# through CipherStash Proxy and dumping the resulting (id, plaintext, +# payload jsonb) rows as a SQLx migration. +# +# Prerequisites: +# - mise run postgres:up +# - EQL installed (e.g. via mise run reset + psql -f release/cipherstash-encrypt.sql) +# - mise run proxy:up (Proxy on localhost:6432) +# - mise run build (produces release/cipherstash-encrypt.sql) +# +# Output: +# tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCHEMA_SQL="$SCRIPT_DIR/encrypted_int4_schema.sql" +OUTPUT="$REPO_ROOT/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql" + +# shellcheck source=_generate_common.sh +. "$SCRIPT_DIR/_generate_common.sh" + +if [ ! -f "$SCHEMA_SQL" ]; then + echo "ERROR: $SCHEMA_SQL not found." >&2 + exit 1 +fi + +# 14 values: includes negatives (boundary), small/medium/large/extreme. +# Chosen so range pivots produce distinct cardinalities — see plan in +# docs/superpowers/plans/. +VALUES=(-100 -1 1 2 5 10 17 25 42 50 100 250 1000 9999) +ROW_COUNT=${#VALUES[@]} + +echo "==> Applying fixture schema (drops + recreates bench_int4)" +psql "$PG_URL" -v ON_ERROR_STOP=1 -f "$SCHEMA_SQL" + +restart_proxy_and_wait + +echo "==> Inserting $ROW_COUNT integers through Proxy (encrypts encrypted_int4)" +# Proxy's eql-mapper cannot unify negative integer literals (parsed as +# UnaryOp(Minus, ...)) with the EQL int column when sent via the simple +# query protocol. Send each value over the extended protocol via psql's +# \bind meta-command so the parameter type is communicated as a binary +# int4 instead of being inferred from SQL surface syntax. +INSERT_SQL=$(mktemp) +trap 'rm -f "$INSERT_SQL"' EXIT +for v in "${VALUES[@]}"; do + # Use literal $1/$2 as bind placeholders; \bind supplies their values. + # \g executes the buffered statement; \bind discards bindings after \g. + printf 'INSERT INTO bench_int4 (plaintext, encrypted_int4) VALUES ($1, $2) \\bind %s %s \\g\n' "$v" "$v" >> "$INSERT_SQL" +done +psql "$PROXY_URL" -v ON_ERROR_STOP=1 -f "$INSERT_SQL" >/dev/null + +echo "==> Dumping $ROW_COUNT rows to $OUTPUT" +cat > "$OUTPUT" <
Done. Wrote $ROW_COUNT rows to $OUTPUT" diff --git a/tests/benchmarks/.env.example b/tests/benchmarks/.env.example new file mode 100644 index 00000000..fe41909a --- /dev/null +++ b/tests/benchmarks/.env.example @@ -0,0 +1,7 @@ +# CipherStash Proxy credentials +# Get these from https://dashboard.cipherstash.com +CS_CLIENT_ACCESS_KEY= +CS_DEFAULT_KEYSET_ID= +CS_CLIENT_KEY= +CS_CLIENT_ID= +CS_WORKSPACE_CRN= diff --git a/tests/benchmarks/.gitignore b/tests/benchmarks/.gitignore new file mode 100644 index 00000000..9e7d7623 --- /dev/null +++ b/tests/benchmarks/.gitignore @@ -0,0 +1,6 @@ +# Generated reports (too large for git, regenerated on demand) +reports/* +!reports/.gitkeep + +# Local Proxy credentials +.env diff --git a/tests/benchmarks/README.md b/tests/benchmarks/README.md new file mode 100644 index 00000000..69087bdb --- /dev/null +++ b/tests/benchmarks/README.md @@ -0,0 +1,37 @@ +# Benchmark Utilities + +This directory contains the Dockerized support stack for generating a 100K-row +encrypted benchmark dataset through CipherStash Proxy. + +The committed automated benchmark coverage lives in the SQLx bench/regression +suite (`mise run test:bench`). `mise run bench:full` is a convenience wrapper +around that existing suite; it does not consume the 100K Docker dataset. + +## Local usage + +```bash +# Populate credentials for the Dockerized Proxy +cp tests/benchmarks/.env.example tests/benchmarks/.env +# Edit .env with your CipherStash credentials + +# Start bench-postgres + bench-proxy and wait for host-side readiness checks +mise run bench:up + +# Build EQL and generate the 100K encrypted dataset in bench-postgres +mise run bench:generate + +# Run the committed SQLx bench/regression suite (10K fixture-based) +mise run bench:full + +# Tear down the Dockerized benchmark stack when finished +mise run bench:down +``` + +## What each task does + +- `bench:up` starts `bench-postgres` and `bench-proxy`, then probes them from + the host with `psql`. +- `bench:generate` installs the built EQL SQL into `bench-postgres`, applies + `schema.sql`, and inserts 100K plaintext rows through Proxy on `localhost:6433`. +- `bench:full` delegates to `mise run test:bench`, which runs the committed + SQLx benchmark/regression suite against the normal local test database. diff --git a/tests/benchmarks/docker-compose.yml b/tests/benchmarks/docker-compose.yml new file mode 100644 index 00000000..d67aca7c --- /dev/null +++ b/tests/benchmarks/docker-compose.yml @@ -0,0 +1,54 @@ +services: + postgres: + image: postgres:17 + container_name: bench-postgres + command: > + postgres + -c track_functions=all + -c shared_preload_libraries=pg_stat_statements + -c pg_stat_statements.track=all + -c pg_stat_statements.max=10000 + ports: + - "7433:5432" + environment: + POSTGRES_DB: cipherstash + POSTGRES_USER: cipherstash + POSTGRES_PASSWORD: password + healthcheck: + test: ["CMD-SHELL", "pg_isready -U cipherstash"] + interval: 1s + timeout: 5s + retries: 10 + networks: + - bench + + proxy: + image: cipherstash/proxy:latest + container_name: bench-proxy + ports: + - "6433:6432" + environment: + CS_DATABASE__NAME: cipherstash + CS_DATABASE__USERNAME: cipherstash + CS_DATABASE__PASSWORD: password + CS_DATABASE__HOST: postgres + CS_DATABASE__PORT: 5432 + # EQL install is performed explicitly by generate.sh before schema.sql runs. + # Leaving Proxy's own install off avoids racing against generate.sh. + CS_DATABASE__INSTALL_EQL: "false" + CS_CLIENT_ACCESS_KEY: ${CS_CLIENT_ACCESS_KEY} + CS_DEFAULT_KEYSET_ID: ${CS_DEFAULT_KEYSET_ID} + CS_CLIENT_KEY: ${CS_CLIENT_KEY} + CS_CLIENT_ID: ${CS_CLIENT_ID} + CS_WORKSPACE_CRN: ${CS_WORKSPACE_CRN} + depends_on: + postgres: + condition: service_healthy + networks: + - bench + # No in-container healthcheck: the current cipherstash/proxy image does + # not ship `nc`, so readiness is verified from the host by `bench:up` + # using `psql` against localhost:6433. +networks: + bench: + driver: bridge diff --git a/tests/benchmarks/generate.sh b/tests/benchmarks/generate.sh new file mode 100755 index 00000000..595e8d10 --- /dev/null +++ b/tests/benchmarks/generate.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generates a 100K-row encrypted bench dataset via CipherStash Proxy. +# No dump is written in v1 — the Tier 2 workflow regenerates fresh each run. +# +# Prerequisites: +# - mise run build (produces release/cipherstash-encrypt.sql) +# - docker compose -f tests/benchmarks/docker-compose.yml up -d --wait +# - tests/benchmarks/.env populated with CipherStash credentials + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EQL_SQL="$REPO_ROOT/release/cipherstash-encrypt.sql" +SCALE="${1:-100k}" + +case "$SCALE" in + 100k) ROWS=100000 ;; + *) echo "Unsupported scale: $SCALE (only 100k in v1)" >&2; exit 1 ;; +esac + +if [ ! -f "$EQL_SQL" ]; then + echo "ERROR: $EQL_SQL not found. Run 'mise run build' first." >&2 + exit 1 +fi + +PG_URL="postgresql://cipherstash:password@localhost:7433/cipherstash" +PROXY_URL="postgresql://cipherstash:password@localhost:6433/cipherstash" + +echo "==> Installing EQL into bench-postgres" +psql "$PG_URL" -v ON_ERROR_STOP=1 -f "$EQL_SQL" >/dev/null + +echo "==> Applying bench schema and Proxy search configuration" +psql "$PG_URL" -v ON_ERROR_STOP=1 -f "$SCRIPT_DIR/schema.sql" + +# Proxy caches the encrypt config at connection-handler init. add_search_config +# in schema.sql writes the new config but the Proxy will keep running in +# PASSTHROUGH MODE (inserts pass through unencrypted) until it reconnects. +# Restart and wait for it to come back before driving the INSERT. +echo "==> Restarting bench-proxy so it reloads the new encrypt config" +docker restart bench-proxy >/dev/null +for i in $(seq 1 60); do + if psql "$PROXY_URL" -c 'SELECT 1' >/dev/null 2>&1; then + echo " Proxy ready." + break + fi + sleep 1 + if [ "$i" -eq 60 ]; then + echo "ERROR: bench-proxy did not come back up after restart" >&2 + docker logs bench-proxy 2>&1 | tail -20 + exit 1 + fi +done + +echo "==> Inserting $ROWS plaintext rows through Proxy (this encrypts them)" +# generate_series emits plaintext rows; Proxy intercepts and encrypts each +# column per the search config applied in schema.sql. +psql "$PROXY_URL" -v ON_ERROR_STOP=1 -c " +INSERT INTO bench (encrypted_text, encrypted_int, encrypted_bigint) +SELECT + ('text_' || (((gs - 1) % 1000) + 1))::text, + (((gs - 1) % 1000) + 1)::int, + (((gs - 1) % 1000) + 1)::bigint * 1000000000 +FROM generate_series(1, $ROWS) AS gs; +" + +echo "==> Creating indexes and running ANALYZE" +psql "$PG_URL" -v ON_ERROR_STOP=1 -c " +CREATE INDEX IF NOT EXISTS bench_text_hmac_idx ON bench USING hash (eql_v2.hmac_256(encrypted_text)); +CREATE INDEX IF NOT EXISTS bench_text_ore_idx ON bench USING btree (encrypted_text eql_v2.encrypted_operator_class); +CREATE INDEX IF NOT EXISTS bench_int_ore_idx ON bench USING btree (encrypted_int eql_v2.encrypted_operator_class); +CREATE INDEX IF NOT EXISTS bench_bigint_ore_idx ON bench USING btree (encrypted_bigint eql_v2.encrypted_operator_class); +CREATE INDEX IF NOT EXISTS bench_text_bloom_idx ON bench USING gin (eql_v2.bloom_filter(encrypted_text)); +ANALYZE bench; +" + +echo "==> Done. Rows: $ROWS" diff --git a/tests/benchmarks/reports/.gitkeep b/tests/benchmarks/reports/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/benchmarks/schema.sql b/tests/benchmarks/schema.sql new file mode 100644 index 00000000..88bb7be4 --- /dev/null +++ b/tests/benchmarks/schema.sql @@ -0,0 +1,81 @@ +-- Bench schema for Tier 2 benchmarks. +-- Applied against the bench-postgres container AFTER EQL has been explicitly +-- installed by generate.sh (see Task 4 — generate.sh installs +-- release/cipherstash-encrypt.sql directly, not relying on Proxy's async install). + +DROP TABLE IF EXISTS bench; + +CREATE TABLE bench ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + encrypted_text eql_v2_encrypted, + encrypted_int eql_v2_encrypted, + encrypted_bigint eql_v2_encrypted +); + +-- Idempotency: clear any prior bench search-config rows so re-running the +-- generator against the same container doesn't error with "... index exists +-- for column". EQL uninstall drops the schema but not public config rows. +SELECT eql_v2.remove_search_config('bench', 'encrypted_text', 'unique') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_text,indexes,unique}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench', 'encrypted_text', 'match') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_text,indexes,match}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench', 'encrypted_text', 'ore') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_text,indexes,ore}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench', 'encrypted_int', 'unique') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_int,indexes,unique}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench', 'encrypted_int', 'ore') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_int,indexes,ore}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench', 'encrypted_bigint', 'unique') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_bigint,indexes,unique}' IS NOT NULL + ); +SELECT eql_v2.remove_search_config('bench', 'encrypted_bigint', 'ore') + WHERE EXISTS ( + SELECT 1 + FROM public.eql_v2_configuration c + WHERE c.data #> '{tables,bench,encrypted_bigint,indexes,ore}' IS NOT NULL + ); + +-- Proxy search configuration: tells Proxy which index terms to generate +-- for each column when plaintext is inserted. +-- +-- Signature: eql_v2.add_search_config(table, column, index, cast_as) +-- (see src/config/functions.sql). add_search_config calls activate_config +-- internally when migrating=false, so no explicit activate_config call. + +-- text column: equality (hmac), pattern match (bloom), ordering (ore) +SELECT eql_v2.add_search_config('bench', 'encrypted_text', 'unique', 'text'); +SELECT eql_v2.add_search_config('bench', 'encrypted_text', 'match', 'text'); +SELECT eql_v2.add_search_config('bench', 'encrypted_text', 'ore', 'text'); + +-- integer column: equality + ORE range/ordering +SELECT eql_v2.add_search_config('bench', 'encrypted_int', 'unique', 'int'); +SELECT eql_v2.add_search_config('bench', 'encrypted_int', 'ore', 'int'); + +-- bigint column: equality + ORE range/ordering +SELECT eql_v2.add_search_config('bench', 'encrypted_bigint', 'unique', 'big_int'); +SELECT eql_v2.add_search_config('bench', 'encrypted_bigint', 'ore', 'big_int'); + +-- Indexes (created after data load in generate.sh, after ANALYZE) diff --git a/tests/docker-compose.proxy.yml b/tests/docker-compose.proxy.yml new file mode 100644 index 00000000..d64e6a8e --- /dev/null +++ b/tests/docker-compose.proxy.yml @@ -0,0 +1,35 @@ +services: + proxy: + image: cipherstash/proxy:latest + container_name: cipherstash-proxy + ports: + - "6432:6432" + environment: + # Proxy connects to the existing tests/docker-compose.yml Postgres, + # reaching the host via host.docker.internal. POSTGRES_* values come + # from mise.toml [env] block (overridable per shell). + CS_DATABASE__NAME: ${POSTGRES_DB:-cipherstash} + CS_DATABASE__USERNAME: ${POSTGRES_USER:-cipherstash} + CS_DATABASE__PASSWORD: ${POSTGRES_PASSWORD:-password} + CS_DATABASE__HOST: host.docker.internal + CS_DATABASE__PORT: ${POSTGRES_PORT:-7432} + # EQL installation is handled by the existing reset / install flow; the + # Proxy must not race against it. + CS_DATABASE__INSTALL_EQL: "false" + # CipherStash workspace credentials are read from the host shell + # environment (mise / direnv / profile). No .env file is required. + CS_CLIENT_ACCESS_KEY: ${CS_CLIENT_ACCESS_KEY} + CS_DEFAULT_KEYSET_ID: ${CS_DEFAULT_KEYSET_ID} + CS_CLIENT_KEY: ${CS_CLIENT_KEY} + CS_CLIENT_ID: ${CS_CLIENT_ID} + CS_WORKSPACE_CRN: ${CS_WORKSPACE_CRN} + # Optional: pin the CTS region host for workspaces in non-default regions. + CS_CTS_HOST: ${CS_CTS_HOST:-} + CS_ZEROKMS_HOST: ${CS_ZEROKMS_HOST:-} + extra_hosts: + # Linux compatibility; macOS / Windows resolve host.docker.internal natively. + - "host.docker.internal:host-gateway" + # No in-container healthcheck: the current cipherstash/proxy:latest image + # lacks busybox nc, so any TCP probe inside the container fails even when + # the proxy is listening. Readiness is verified from the host by the + # proxy:up task using psql. diff --git a/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql b/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql new file mode 100644 index 00000000..7556d9e6 --- /dev/null +++ b/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql @@ -0,0 +1,29 @@ +-- AUTO-GENERATED by tasks/fixtures/generate_encrypted_int4.sh +-- DO NOT EDIT BY HAND. Re-run the generator to refresh. +-- +-- Source: 14-value integer set defined inline in the generator. +-- Produced via CipherStash Proxy (HMAC + OPE terms). +-- Used by encrypted_int4 domain SQLx fixture tests. + +DROP TABLE IF EXISTS encrypted_int4_plaintext; + +CREATE TABLE encrypted_int4_plaintext ( + id BIGINT PRIMARY KEY, + plaintext INTEGER NOT NULL, + payload JSONB NOT NULL +); + +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('1', '-100', '{"c": "mBbKSl1A>nJLHvy{R_+!y+r237Oz{OOyZusRU1%=^N29ellMsHtFke~AjFeV1#(=nP!7;6lb4dK$exS)cVyEoXZr0(3=TevwytGvV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "c8faf5849bba756007c19df73eb704aa640dea7eec353af533b4502cc354640d", "ob": ["a1a1a1a15481492c65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fef2c4558f8c98caf3d95b2a84cfd8d1bf34d597f6b0c7cec23413e27dcb5fea3fa07832f2566f859ed23a16910615e5364d4ac48ae66b56b98836c956171ab4d6cf6eacaa0b1c7b5bae407558a7751d01a3312a765d38eabd8f55815ff310aeab71a3e802ad78088941fb5dbf1867d548b10969184d3a56b71bcbae6f31cec20585879ed7da7174b4bbf9d60080b5ba9ecd38473e20631bbfb4d4883fca75cdf7e4727c85f669e3bc8981fa2d7b6d2309793c86ccdb1fa1fb5e352b4298b138bdee80b603ca95ced5a3a6788048b8922b7f679a524270f36e44ba6006303b494e1e3b6efb9878ce9da67942d1b7c5e1d2ff8d337be041c00f85317c66144171419aa7577d5a717d569c0d5dfe9fa5d52bdf449fa4043a4258c98514eb81abda3b05213f1d096f8e63f7ed72fc7d8f7d88464484bc8d75acc26688770b3f68d699383a92037bc5b015f723aa4a0a7d9073"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('2', '-1', '{"c": "mBbLfH%e(Q?p_s?bNQ{<#{uC8TnV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "f6bde997370f33b54e4ca61473609b197879c18e5197548176518b714b119146", "ob": ["a1a1a1a15481495d65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fef2c4558f8c98caf3d95b2a84cfd8d1bf34d597f6b0c7cec23413e27dcb5fea3fa07832f2566f859ed23a16910615e5365295862e7ec0da68e2a3decc06d1d557b6922aae88582b92f00da5cf7b0e22fef74f1ed895c9164c470aea01637f1d726adaa19b9413d51b2e80f9e6c9fdc1f74a576aabbd6f183d9c6de23082f281d85a5dacf3c5aa719d1ad4a8f22aeb281587ea04465e7d99b73daa382f44af6cc6b300698cdd517f908f618cea3c3733ecccdaf649b4cf9355a9d27bc81c64563e6f177b8755b3aff1bd4549d0e572fe44b752fb8d0e96fd76a5be31a4945ed2ceda86d928aba563485661bcd2ea533396be44c4cd918a5e7eef8dfbb10982ed3eaa49b35036ef673a21ff39e26fe62860d8219ee9aa001e094ce1131ebe5aaef33f8e933a9eb6003c4b9a656043a9cd8723d60174e429f998f37e7317f27a05b68ecbb3c6a4944f062875b85d2d7131da"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('3', '1', '{"c": "mBbLYhRor}6CjJ;AsHY+(?Ng47K{2;`qaGoX)GjDl0Y0;`k@_)eA52JATQHQY^bSRimoI>d;+&&X6|8PG@TT7Dsa8|UCWFmT&`tqV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "e166c0aac72bd6715d32ef1601aa5333531ead4620a5f29da2c1de3488f4c567", "ob": ["a1a1a1a1964545e965cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5af190366875ddb8de5697963484becd5ebb1925d11d89ea7ec7949dc519e7596d9091bf1241fafbc5da30c777b671912ce3e0c372c0ed63fd8499fee2dd5e3934ac2f0f6c4f84a698d76e0fa3a60c19f48dd672705751068bdb2846bd092f5122b1260d9e42cefc95d66b9e2376e6b0a982046952cea11611dbc2a12aac49f2111095cdbe647100a9959bd720d03477ed4fd27e062f65d7b7fdbc9a44d2033563b603a4f3cfe1fcda82cf1730c40f01d92141f4611d6b00dfb08298a41533f7369965867af4a9fba4dbaba1467a2e3b923f970a9c08370b45ae608445f8cab67f1085291ebbf194435b0606b49e8771eb5cfc20cb51c79ff273a77b730fc35fc07c62bb21330260a44e3dc132b19cc7e7874643ffc27ccbb41375abe6c302b009"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('4', '2', '{"c": "mBbM0cqpOq3a!Xis`V0^%ic<-7|Nd29<6#ESv$eRAST6LAHL`!o;_oQUU9dZbi{(cCVQ)i*P2|4WORRB5UyozV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "84507797349e30049201af1f46c5e2e47f4660d18726716a0b56427c0b9a021b", "ob": ["a1a1a1a19645458b65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a513c8a4a70ef54eb8ba541efae0e03dd2fe9264d04a5ef27b7da6eb22e3e902476b6a1bb794b88bae7a12f5341b8c5ea582a5b9d244cd2e6a72e1bc9dca08af01b426d7a918cf1b3f809b957b3083b2d4d93d296c038b17bbd3bdc956a2f97d9791368528ddcc6d87c5b1f8a34f54662ea8f3f8d24ebf67ff186e3ed7ac6e03a94132f7d4e7d7061a53d8013aa36caa74bc98dda9106ad9cf37cfed6a7ff7f8e1270bc9d413251eb1f43ed53fc5a15dfa9d53a8e79478de5a9447c5135d8b8325a5fe397fdb796630bc2fa5e06144fdbd49a576a4833bfd0fc1c89246c9fc807064d10bc3b5d18d3d1f1e07fadb1bd719db0310aa8573a5e7baf1e5fa3d4e791313c225c47cf39089e70d71ee1801c10f278b37eb23afebe904e4f89953d4a64"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('5', '5', '{"c": "mBbKeYS{+(Zso@h#h(k`Pj$@178N4lo-=GaVRpm;j@X|!wb;*>4Co2OARQ#xz%i*M8ke;~Gda*nx5p{`|aV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "fd87f32d954e6b8114710c1cb421b25f9c32ff08cf0b69ba68053ef2a32ec854", "ob": ["a1a1a1a19645459e65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5aa803720971383049abfacef1eae1766a2d5bd879296124ac6930a8cdb4dbe1059aabb4b89fc13888fc4b60fa4498fdecb008eb20c7ae868fa21ba4d55cb69a75a30eaa52f5633c290dffbb6453c35c11fb67fef2a62cabb7a5653e3c3719aa5d805f2fdf80eb05ab66baa6b7f7b3956e71bea6fe97aba914e89e96e4fb6fcf1153f5fd64b8217f18836536df493179b93215ad84e36215c80c2eac0446d6ceb8d1bc26bf325fb597b8bce8192876f2121524d00068bb05b97a7965d982022c61c0fc76ecd90a211587ed344554810dcf102902718a5ed127eacbb43d3030d4ecfa9a6e86541675286dcaa330ae210b3e3d8a5a89f436ac2daa80a1e3f2bd84c4772c417fa71863b43d6a4b8e0e77051240fd73bb3b66eb03a70196ef6f7ffcf8"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('6', '10', '{"c": "mBbJ$2dznX$8+hEVm#p*84AqA7WaqRu6P1z-60C!XlFVZGl#oq!c!{5AkID7t=)F>bIS!jr`cENW5zfF#$D)=RI@5fzL6JWpRQ$YV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "9dfab57faa412c407c8b7a8c6f988afc15f9ab25e604fc6542eb84b00f52e2d9", "ob": ["a1a1a1a19645457265cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a3113f16bb322e59b86500da7574b52d298792c385a5d0355b9ccea9127c7f17fae5686334ec7416bd4da7f31ffe18b8dab571880bd0a11d8dadef2b8210123311a855d8db9c3254af31572e6909f2ec36975aa2e2df540fd7e47398b2dbbb911711c241ad9e2f8471b63e8b74adb6e8c1c10291740faebf8e3c58e2100682e750301eb6807e1723a8a0f228cc3af70860d8b2d7d5efbb8ed2ebad31656f9f31cc20fe49139dd0e97bfeac1d0f599d40e24b6222549b0793be4a19c7f8ebac901021a70eff61a0838eff9acec17d78007237034d10a0672cec37205b095eb80480613da7f4d7139241a274750db8a4e0792e1447f5d52887cbdc935e159c8e13107d26d33dca94518bb4693f7fb6e059c1222561255ccb5f33d4ea9fb187721db"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('7', '17', '{"c": "mBbKs&oax}9^^|SIisV#TLyx}7GwgnjEjg#`hWPogd*#F6}hSJWVEy(B19mLgF*=B&)fehTfyWDL@xFRo>7V{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "0ec321b40a8ae7a37bfd557d8451714996b2986447d32d6a6b3ba91492be0bfe", "ob": ["a1a1a1a1964545ee65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a2b59b0c0132c8308a958946dba0cabf8b5ee08e2c8a7d77f9fe513bf5e8e7da0fecb176102b68aee30552310f7d56af4e973dc54cf43e9d83a72ea3fd8988c7263ec26abd96a3f5afa1b7abfe2c4cee5ed7498c6ea3cc189a1cbac98d9044145dc7598710e35a6ba8bae587c1f115296172695b48c26d4a3497a65cfcb98d63f5e91c34ba1a921883152c1541b85a12bea47bf7de15a9ddb01c8699f93850b42f95cf4f7b5ec56e5fa952235804cd306d5e654da2460a403f81db4aa3b6e667bc5315997e7b69f36acf6b2fc8ba68cfabfb846dbbf124db55cae63f4e38fdbe83d7fc54579b5ba0f2f282847c1a3b3fe4235c5dd1da69e3f1b33eeb6c31fe67ce5f333cd797e2d2aa6d702e9236f7c310b1dc3ee682ff09187c3eb73d11aa72a"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('8', '25', '{"c": "mBbKzuk={LFxu?QubtJ*_sx6=Y5HPpN6zXtgdBlV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "3a8ce9251f3ee4904d625b2e79f9eb930c2b3086ae641f934260c9dd3cf0ecb6", "ob": ["a1a1a1a19645454c65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a542bc1720f618d33b43760d45700da56fa52e146601e4e87d97b3a10dd585cb3b0bdc9d13f54e63f4e30326b387b0b4003113da12a722073be23eeb6b6f345e5e0d895ddabc5307b4bee23671ea4793812387c8db666050e8a2c634b5e4bc8b243673485cfc669288dec12bebb53dc5ea0cb079bb0af43b4bac6eb9b52853a7326d9528fe05651c3ee412e855694236d4fcd3192daa776e03bac4852f7310146998dfb0cf39e4e1e6cdb0b329b278472036e751b50a0d43e7b11717a856e93d6f73b4ef447b5ad8f4593f1e8bfbbf6d848cd6887dfd44e03ee79b72b39814071a5f70a5622dea85a6fbeec304a61a5ab4d213e959d2da156323385155c412e94b25b9e7399cb5c249506a17ca65c2a3cd2d49a1ebe99357c7707b5b6e4996f65"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('9', '42', '{"c": "mBbK1bx7W;#5vev=wY1!*a6nW7FhjWI3X9$KbVSRXClSn+{r!8n_e@-AhE3r2#bvbwxh}tD#M--AsG_DqD@Tm2IiPaRrA6Os;*^jV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "231bcb9386ea23eb2630335bc3fa7542b4bec54e5251cec202a10c53453f5b65", "ob": ["a1a1a1a19645450665cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a296cf506a6902dcd1010e8ea6daf751d025252d13f2a5f7ca6f5673aa8bda37cae6d0c2ae66eb602ed1a8922741a8a931a00682f7a84e97267b19e4d3458bd5cc5a54d8fb3a793f0627943eba3318311ea33091db781ccd8c630cb8ff1e5b3edeec7022cfe1a06c987e512f5198825eb659c6206fd6f0197de3e791d52dda77c20adac3d21d3b13acb9fb3742121befe2504ed1c5d150799176b175bf2b82a6ebd5224dab890c323cef7b3e5482eb0efaaf1bc2047afc32345c056e95312c37ece65ba637d452035540e95b338058bad657c2e105b120e30ce76dbbec3a145d491eb5513e69c40dd43a88ddeff386644df2fbd167b7bef640041f9cb57a8174d479954e45e82d3edf5be3637301b5a2c1f633565218618edae2fd75e6048bf0a"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('10', '50', '{"c": "mBbLubhR#eRIBRKP`Za3N&ddX7R78Ur?1=??q*`{-W{g@?u})F3=x0CAOaUdoNrgR-V;Hq{g-NbrRAu#Tm_n~lb?6ctYlh&Os-{aV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "4a040d1a9eb01293059eaf834e84822b97de2dd7c6839d8cdae3cf8e0ef1ab81", "ob": ["a1a1a1a19645458c65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a0f4a253e2c8c48870919a382d5b81237d26b63103d7fd84d4077b73f7450239161bbb9cc31c2950d950a1f08897773eff96077f025341ee7985c264c4b9647b7e43c0e17900bdb6d4102d3ccf068ce620d26db623f44d9f5893024538009685c1f178793bfc6b8870cedb33b293a6f5c28f353a4bd0a5bc3979f159399058bb21116b011cad86bb1973be07c3976c2ca262d9fad6fb4903e412eda1839eb30c3439ac51e782e3fab35e517ead7c8b630c490a24aaea97677b4d043f429c0ee4e62a3fe71649c12fecfb7cc8473293ec8f9c9faff8e55c7c5bf4a15c6cbfd4ede19ad2d4f7a29d2c059dbe3f040913ff3c755da18a94182f8d58b94e41770f6c3d8013744e11f8890616e56d380116a7eab2e012364d5a270e8f304b9425ea94d"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('11', '100', '{"c": "mBbL!LwVV+Vl8*@^yEs2*qD*T7PpCgPf=vlV?t2SQ$v0`*^g)EmVCOzAcUF(h2<1yqODlK0-}Arf|cwjcIx+e2jMFj0t9PNsjg*iV{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "2fc081842f8545a18689b5eaccca41a935f014ad6d0456a924c4fa8d445833bf", "ob": ["a1a1a1a19645451c65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5a8fcbebfffe2a0e9c5c05d2eebc34586707605468394a274d4ce84c73300754d90e9904f88788c32137c8207791c44b2752c20812a6ca0201db7f752f944616ab76dbbbfb4924ceae1de65e7a1c38a1b65b0574b5f07ef207a90065c42f4fa65a690e1dca4e037288b70e9a822052953c2a89711f3180c48718689db40a8d8b5715bc16bb5660cbe3484bad5f833b7f4eb57a87310f85299b501ce1efcf26903a448a9187330e2b18f2b0751003c0edf6cf0b99d1d494bf047ac3e204b94ae6aa77a85c9a443de97f7918cb5150a9c59367945d7ae89e18e2a62ad23ecd66f7d32c28ca60728db1c5815293616e4ad1de3c2d74dfdfb839216cb8dc4fc4f49269e39fc1d2f122aff77f0b0e1bfab4ef4432a4e0d3368b4366632b880e52ed89ba"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('12', '250', '{"c": "mBbK#R%P_`%atBydMup-mD_H_7Li%m8A|F;dvA>Y_^QvD>ppyX=lMOvAWh@0st_N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "5f1bd84cff5e65d213b7a5c4005f442d17f169e9e92800598adadf37cff71de6", "ob": ["a1a1a1a19645452e65cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f1888b615f7526dbcd21f213f1891df5af19c5d189cde4caa7b19cbbb583b33b8dd7bfb4df5f4508c38e1c94e477d128d1873781a3428ec43a7ccc6f55a18d784692f258c2f33bef5cb0997fe486e3b40a5a128db7e5b09db5627ee46a55258e928aa135e0285e764a5ee6aa72311bd810dae9ae2747ce6c4ec2bac140927eaa992cc85d8e9b3c0157cb4a984f58b4fd21ca5e4898cf6a61e7de5bdaa9e045ab546afb65b509f835b023439a2c962312872d38c92357cf83e3475e48450f59197edb6039b25b5abc87e6e58fcbcc42a7b4cdad8d1df6886ba577957207921b53ba1e53830156f116d751ca64138f8936e53a7831ef5fa0819aeb78243579a15383cd55d3d5449b78b01019a35ca9c70d083a2f302f3c3ef7c7ad4ef0a4e8457c552c0d16e43fa7d103ae0171644e00b88"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('13', '1000', '{"c": "mBbKDm$u6vOxUbP-P&WfMw8RT7QXc>s7fP*`bW>P>jPt179vf<5p1l)AiL0!$;tL1C}3Hyy*?tEPN2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "72a100399ebc0cbbf13b13384fbd0a06c7e1c9431ac1204888fc3ee699729f93", "ob": ["a1a1a1a19645d38465cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f6e67f6694308e6798a670a870e41a7dbd333f347fc16f34e9eb2514418afc478cfb9e0893b731832318fe5c01c734eeb58edcb2a49c210e8c80496f45c65ce55bd15fc702bd1a8a50872f2623047bb6138a88291fa548ea837d401d7315be08ae3d725cc9aa0aff3717a6e1254912daa3db212a818b2825055eec4eb6dbc01dc490a727b86e82e51f59cf03c2a76386509d1295d7c76f0474f3216fe77a107a23e88246848bfb86f0494af82b4faf91cabb351db9b568da3eb64ce360d68db724b347da4d844c47439f33585e9f416114ba73973794f2374ed18b0445ed23e6cec6584c23edc1de19e637c4cff55f5203bc104d360ac02b634c11443f110322a1eab498d5ec92abb8c89231f5b3bf10e413fdcbf4ffe41678b66f5a8550c8cce481bbb7e8eae0b6a08ce016f710c4c50"]}'::jsonb); +INSERT INTO encrypted_int4_plaintext (id, plaintext, payload) VALUES ('14', '9999', '{"c": "mBbMDBL-q!6!rQwR5A43Qhsg37M6J*x1=!!pwQ*)It`0cHfasj9oF;2AfOcWDJ#~`f+FVKw-a876;D&NwU@urcv|D7XcB8Ad9Gz{V{&N2h#1KJ4(b;4eKp?Aiw4IQd+|>", "i": {"c": "encrypted_int4", "t": "bench_int4"}, "v": 2, "hm": "805123862649caceb6c13fd53c3ea9f9e0888986cdc951cf50a820632aafb2d8", "ob": ["a1a1a1a19645b89065cecfeb3421313bec09225b7928c0122f3a5d81152b1289903ac6485ca9843c2de97fa9663be406a80b7d54a06ab01a16abf5fdcab6b6f5a2a48ba6fd34c0fe6bc8785683b327c03f13eb22dad68591e08a40c49634b09f96c8ac98661a5d2f68b1b4ca492400160ebc035ff9744a665420b36ba9eac70df9c96e41e3cc812fd5b754a8970b2f6aab01a2b4285f7aa61290f6aadf52c5c2bd986a2d61af3fe3feba900062db9674416dc29d4d4dd5e7f943a2bf458b9a4baefde3410fd713311c0d1eab7b8039922cb0721969b6fad58986a692110ca39e2293b4dc8022c8518d0bd4615e59897d9776875df6cdb835fbc1cfc69c481d5f48602a4e6da42f953f55add7b1f920d4683747eee2d0f99a9a0ad40827375edfc85ee55cf6acb4bcdc6fc5e2a69a565e7185c86b94a86acdf36be9abd7c843373d0906fd59ec38cf9d2dfd0dd8e0bd85e7525c5c02c8c4d4fb0e6fe5a211b89383a499cbb63820daccd5c37d06f4cf9faa0796eaaef8ae34617ceead72f6ef37cf0c5b5de19234e04bc3e53c92a4f6512ddd0b799fb59839"]}'::jsonb); diff --git a/tests/sqlx/tests/bench_data_tests.rs b/tests/sqlx/tests/bench_data_tests.rs index 0295eaec..c81d6d0d 100644 --- a/tests/sqlx/tests/bench_data_tests.rs +++ b/tests/sqlx/tests/bench_data_tests.rs @@ -20,6 +20,30 @@ async fn fetch_sample_encrypted_text(pool: &PgPool) -> Result { ) } +#[sqlx::test] +async fn benchmark_schema_can_be_reapplied(pool: PgPool) -> Result<()> { + sqlx::query("TRUNCATE TABLE eql_v2_configuration") + .execute(&pool) + .await?; + + let schema = include_str!("../../benchmarks/schema.sql"); + sqlx::raw_sql(schema).execute(&pool).await?; + sqlx::raw_sql(schema).execute(&pool).await?; + + let active_bench_columns: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM eql_v2.config() WHERE state = 'active' AND relation = 'bench'", + ) + .fetch_one(&pool) + .await?; + + assert_eq!( + active_bench_columns, 3, + "reapplying benchmark schema should leave one active config for each bench column" + ); + + Ok(()) +} + // ========== Data Integrity Tests ========== /// Verify fixture seeded exactly 10K rows