-
Notifications
You must be signed in to change notification settings - Fork 1
feat(bench): Phase 2 - benchmark table, 10K row fixture, and verification tests #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
2f9dad0
feat(bench): add benchmark table with 10K rows, indexes, and verifica…
tobyhede 44dabd7
refactor(bench): extract BENCH_ROW_COUNT constant from magic number
tobyhede 1934a91
fix(bench): move 10K row INSERT from migration to opt-in fixture
tobyhede a57682a
fix(test): correct pg_stat_statements_reset argument order
tobyhede 89f86f6
fix(bench): address code review feedback
tobyhede 493f085
fix(bench): address second code review round
tobyhede 2e371e6
docs(bench): address CodeRabbit feedback on fixture docs
tobyhede 53972f9
refactor(bench): use Zipf-like skew for bench fixture distribution
tobyhede File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| -- Fixture: bench_data.sql | ||
| -- | ||
| -- Seeds 10K rows into the bench table for performance testing. | ||
| -- Each column draws independently from 99 distinct encrypted values (ore ids 1-99) | ||
| -- using a Zipf-like skew so the planner sees realistic histograms. | ||
| -- | ||
| -- Index terms per row: hm (hmac), b3 (blake3), bf (bloom filter), ob (ORE blocks), sv (STE vec) | ||
| -- Data generated via create_encrypted_json() from 004_install_test_helpers.sql. | ||
| -- | ||
| -- Distribution: | ||
| -- Deterministic via setseed(0.42) — byte-identical across runs. | ||
| -- random()^2 produces a power-law skew: P(id=k) is proportional to 1/sqrt(k). | ||
| -- Top id gets ~5% of rows (~500); tail ids get ~0.5% each (~50). Ratio ~10x. | ||
| -- Three independent draws per row decorrelate the columns. | ||
|
|
||
| SELECT setseed(0.42); | ||
|
|
||
| INSERT INTO bench (encrypted_text, encrypted_int, encrypted_bigint) | ||
| SELECT | ||
| create_encrypted_json(1 + floor(99 * power(random(), 2))::int), | ||
| create_encrypted_json(1 + floor(99 * power(random(), 2))::int), | ||
| create_encrypted_json(1 + floor(99 * power(random(), 2))::int) | ||
| FROM generate_series(1, 10000); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| -- Fixture: bench_setup.sql | ||
| -- | ||
| -- Creates benchmark indexes and refreshes planner statistics. | ||
| -- Table DDL from migration 007_install_bench_data.sql; 10K rows from bench_data.sql fixture. | ||
| -- | ||
| -- Indexes: | ||
| -- bench_text_hmac_idx - hash on eql_v2.hmac_256(encrypted_text) for equality | ||
| -- bench_text_ore_idx - btree on encrypted_text via operator class for text ordering | ||
| -- bench_int_ore_idx - btree on encrypted_int via operator class for range/ORDER BY | ||
| -- bench_bigint_ore_idx - btree on encrypted_bigint via operator class | ||
| -- bench_text_bloom_idx - GIN on eql_v2.bloom_filter(encrypted_text) for containment | ||
| -- | ||
| -- Pattern follows containment_with_index_tests.rs: indexes in fixture (not migration) | ||
| -- so tests can verify before/after index creation. | ||
|
|
||
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| -- Migration: 007_install_bench_data.sql | ||
| -- | ||
| -- Creates benchmark table for performance testing. | ||
| -- DDL only — data is loaded by the bench_data.sql fixture so that | ||
| -- only bench tests pay the 10K-row seeding cost, not the entire suite. | ||
| -- | ||
| -- Columns: | ||
| -- encrypted_text - text equality (hmac), pattern match (bloom), ordering (ore) | ||
| -- encrypted_int - integer ORE range/equality/ordering | ||
| -- encrypted_bigint - bigint ORE at scale | ||
|
|
||
| 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 | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| //! Benchmark data verification tests | ||
| //! | ||
| //! Validates bench_data fixture (10K rows) and bench_setup fixture (indexes): | ||
| //! - 10K rows seeded correctly across 3 encrypted columns | ||
| //! - Index terms (hmac, bloom, ORE) are extractable | ||
| //! - Indexes are used by the query planner (EXPLAIN assertions) | ||
| //! - Sequential scan baseline without indexes | ||
|
|
||
| use anyhow::Result; | ||
| use eql_tests::{analyze_table, assert_uses_index, assert_uses_seq_scan, explain_query}; | ||
| use sqlx::PgPool; | ||
|
|
||
| const BENCH_ROW_COUNT: i64 = 10000; | ||
|
|
||
| // ========== Data Integrity Tests ========== | ||
|
|
||
| /// Verify fixture seeded exactly 10K rows | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_table_has_expected_row_count(pool: PgPool) -> Result<()> { | ||
| let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM bench") | ||
| .fetch_one(&pool) | ||
| .await?; | ||
| assert_eq!( | ||
| count.0, BENCH_ROW_COUNT, | ||
| "bench table should have 10000 rows" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify all three columns have non-null encrypted data | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_columns_are_populated(pool: PgPool) -> Result<()> { | ||
| let count: (i64,) = sqlx::query_as( | ||
| "SELECT COUNT(*) FROM bench | ||
| WHERE encrypted_text IS NOT NULL | ||
| AND encrypted_int IS NOT NULL | ||
| AND encrypted_bigint IS NOT NULL", | ||
| ) | ||
| .fetch_one(&pool) | ||
| .await?; | ||
| assert_eq!( | ||
| count.0, BENCH_ROW_COUNT, | ||
| "all rows should have non-null encrypted columns" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify hmac_256 index terms are extractable from encrypted_text | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_encrypted_text_has_hmac_terms(pool: PgPool) -> Result<()> { | ||
| let count: (i64,) = sqlx::query_as( | ||
| "SELECT COUNT(*) FROM bench WHERE eql_v2.hmac_256(encrypted_text) IS NOT NULL", | ||
| ) | ||
| .fetch_one(&pool) | ||
| .await?; | ||
| assert_eq!( | ||
| count.0, BENCH_ROW_COUNT, | ||
| "all rows should have hmac_256 index terms" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify bloom_filter index terms are extractable from encrypted_text | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_encrypted_text_has_bloom_filter_terms(pool: PgPool) -> Result<()> { | ||
| let count: (i64,) = sqlx::query_as( | ||
| "SELECT COUNT(*) FROM bench WHERE eql_v2.bloom_filter(encrypted_text) IS NOT NULL", | ||
| ) | ||
| .fetch_one(&pool) | ||
| .await?; | ||
| assert_eq!( | ||
| count.0, BENCH_ROW_COUNT, | ||
| "all rows should have bloom_filter index terms" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify ORE terms are extractable from encrypted_int (3 of 5 indexes are ORE btree) | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_encrypted_int_has_ore_terms(pool: PgPool) -> Result<()> { | ||
| let count: (i64,) = sqlx::query_as( | ||
| "SELECT COUNT(*) FROM bench WHERE eql_v2.ore_block_u64_8_256(encrypted_int) IS NOT NULL", | ||
| ) | ||
| .fetch_one(&pool) | ||
| .await?; | ||
| assert_eq!( | ||
| count.0, BENCH_ROW_COUNT, | ||
| "all rows should have ORE block index terms" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify ORE terms are extractable from encrypted_bigint | ||
| /// | ||
| /// Both int and bigint columns use the same eql_v2_encrypted type and ob index structure. | ||
| /// These tests verify that data seeding populated both columns, not that encoding differs. | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_encrypted_bigint_has_ore_terms(pool: PgPool) -> Result<()> { | ||
| let count: (i64,) = sqlx::query_as( | ||
| "SELECT COUNT(*) FROM bench WHERE eql_v2.ore_block_u64_8_256(encrypted_bigint) IS NOT NULL", | ||
| ) | ||
| .fetch_one(&pool) | ||
| .await?; | ||
| assert_eq!( | ||
| count.0, BENCH_ROW_COUNT, | ||
| "all rows should have ORE block index terms" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| // ========== Index Usage Tests (with fixture) ========== | ||
|
|
||
| /// Verify hash index is used for hmac_256 equality lookup | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data", "bench_setup")))] | ||
| async fn bench_hmac_equality_uses_hash_index(pool: PgPool) -> Result<()> { | ||
| let encrypted: String = | ||
| sqlx::query_scalar("SELECT (encrypted_text).data::text FROM bench WHERE id = 1") | ||
| .fetch_one(&pool) | ||
| .await?; | ||
|
|
||
| let sql = format!( | ||
| "SELECT * FROM bench WHERE eql_v2.hmac_256(encrypted_text) = eql_v2.hmac_256('{}'::jsonb::eql_v2_encrypted)", | ||
| encrypted | ||
| ); | ||
| assert_uses_index(&pool, &sql, "bench_text_hmac_idx").await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify btree index is used for ORDER BY with LIMIT on encrypted_int | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data", "bench_setup")))] | ||
| async fn bench_ore_order_uses_btree_index(pool: PgPool) -> Result<()> { | ||
| let sql = "SELECT * FROM bench ORDER BY encrypted_int LIMIT 10"; | ||
| assert_uses_index(&pool, sql, "bench_int_ore_idx").await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify GIN index is used for bloom_filter containment | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data", "bench_setup")))] | ||
| async fn bench_bloom_containment_uses_gin_index(pool: PgPool) -> Result<()> { | ||
| let encrypted: String = | ||
| sqlx::query_scalar("SELECT (encrypted_text).data::text FROM bench WHERE id = 1") | ||
| .fetch_one(&pool) | ||
| .await?; | ||
|
|
||
| let sql = format!( | ||
| "SELECT * FROM bench WHERE eql_v2.bloom_filter(encrypted_text) @> eql_v2.bloom_filter('{}'::jsonb::eql_v2_encrypted)", | ||
| encrypted | ||
| ); | ||
| assert_uses_index(&pool, &sql, "bench_text_bloom_idx").await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify btree index is used for ORDER BY with LIMIT on encrypted_text | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data", "bench_setup")))] | ||
| async fn bench_ore_text_order_uses_btree_index(pool: PgPool) -> Result<()> { | ||
| let sql = "SELECT * FROM bench ORDER BY encrypted_text LIMIT 10"; | ||
| assert_uses_index(&pool, sql, "bench_text_ore_idx").await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify btree index is used for ORDER BY with LIMIT on encrypted_bigint | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data", "bench_setup")))] | ||
| async fn bench_ore_bigint_order_uses_btree_index(pool: PgPool) -> Result<()> { | ||
| let sql = "SELECT * FROM bench ORDER BY encrypted_bigint LIMIT 10"; | ||
| assert_uses_index(&pool, sql, "bench_bigint_ore_idx").await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Verify sequential scan without indexes (before/after pattern sanity check) | ||
| #[sqlx::test(fixtures(path = "../fixtures", scripts("bench_data")))] | ||
| async fn bench_hmac_without_index_uses_seq_scan(pool: PgPool) -> Result<()> { | ||
| analyze_table(&pool, "bench").await?; | ||
|
|
||
| let encrypted: String = | ||
| sqlx::query_scalar("SELECT (encrypted_text).data::text FROM bench WHERE id = 1") | ||
| .fetch_one(&pool) | ||
| .await?; | ||
|
|
||
| let sql = format!( | ||
| "SELECT * FROM bench WHERE eql_v2.hmac_256(encrypted_text) = eql_v2.hmac_256('{}'::jsonb::eql_v2_encrypted)", | ||
| encrypted | ||
| ); | ||
| let explain = explain_query(&pool, &sql).await?; | ||
| assert_uses_seq_scan(&explain); | ||
| Ok(()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.