From 03501093e8e3c19bbf967bd026735bfe438da220 Mon Sep 17 00:00:00 2001 From: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Date: Thu, 27 Aug 2026 15:31:59 -0400 Subject: [PATCH] Revert "fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822)" This reverts commit b3baa56ba83d01a0b7292d81aae9370f5616c62b. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> --- crates/buzz-db/src/migration.rs | 44 +++--------------- crates/buzz-search/tests/fts_integration.rs | 5 -- migrations/0033_private_managed_agent_fts.sql | 46 ------------------- schema/schema.sql | 4 +- 4 files changed, 9 insertions(+), 90 deletions(-) delete mode 100644 migrations/0033_private_managed_agent_fts.sql diff --git a/crates/buzz-db/src/migration.rs b/crates/buzz-db/src/migration.rs index 75201afba8..9df02c8abf 100644 --- a/crates/buzz-db/src/migration.rs +++ b/crates/buzz-db/src/migration.rs @@ -645,7 +645,7 @@ mod tests { let mut migrations: Vec<_> = MIGRATOR.iter().collect(); migrations.sort_by_key(|migration| migration.version); - assert_eq!(migrations.len(), 33); + assert_eq!(migrations.len(), 32); assert_eq!(migrations[0].version, 1); assert_eq!(&*migrations[0].description, "initial schema"); assert!(migrations[0] @@ -844,18 +844,6 @@ mod tests { assert!(migrations[13].sql.as_str().contains("search_tsv")); assert!(!migrations[0].sql.as_str().contains("30350")); - // NIP-PMA kind:30179 FTS exclusion (0033): same wrap-the-existing- - // expression shape as 0014 so brownfield databases stop tokenizing - // private managed-agent ciphertext without a policy rewrite. (The - // migration itself still rewrites the events heap and rebuilds the - // GIN index — see the 0033 header for the operational cost.) - assert_eq!(migrations[32].version, 33); - assert!(migrations[32].sql.as_str().contains("kind = 30179")); - assert!(migrations[32].sql.as_str().contains("search_tsv")); - assert!(!migrations[0].sql.as_str().contains("30179")); - assert!(include_str!("../../../schema/schema.sql") - .contains("kind IN (1059, 30179, 30300, 30350, 30622, 44100, 44101, 44200)")); - // Public push-gateway authority is intentionally deployment-global and // durable: immediate revocation and hostile-relay admission cannot be // honestly provided by a stateless gateway. @@ -1927,7 +1915,7 @@ mod tests { #[tokio::test] #[ignore = "requires Postgres"] - async fn populated_upgrade_preserves_search_policy_except_for_private_kinds() { + async fn populated_upgrade_preserves_search_policy_except_for_push_leases() { let pool = connect_test_pool().await; reset_public_schema(&pool).await; MIGRATOR @@ -1943,7 +1931,7 @@ mod tests { .await .expect("insert community"); - for (marker, kind) in [(1_u8, 1_i32), (2_u8, 30_350_i32), (3_u8, 30_179_i32)] { + for (marker, kind) in [(1_u8, 1_i32), (2_u8, 30_350_i32)] { sqlx::query( "INSERT INTO events \ (community_id, id, pubkey, created_at, kind, tags, content, sig, received_at) \ @@ -1970,37 +1958,19 @@ mod tests { .fetch_all(&pool) .await .expect("read pre-push search behavior"); - assert_eq!(before, vec![(1, true), (30_179, true), (30_350, true)]); - - // 0014 fixes 30350 only. A brownfield database that stopped here still - // tokenized kind:30179 ciphertext — the gap 0033 closes. - MIGRATOR - .run_to(32, &pool) - .await - .expect("apply migrations through 32"); - let pre_0033: Vec<(i32, Option)> = sqlx::query_as( - "SELECT kind, search_tsv @@ plainto_tsquery('simple', 'needle') \ - FROM events ORDER BY kind", - ) - .fetch_all(&pool) - .await - .expect("read pre-0033 search behavior"); - assert_eq!( - pre_0033, - vec![(1, Some(true)), (30_179, Some(true)), (30_350, None)] - ); + assert_eq!(before, vec![(1, true), (30_350, true)]); run_migrations(&pool) .await - .expect("apply remaining migrations to populated database"); + .expect("apply push migrations to populated database"); let after: Vec<(i32, Option)> = sqlx::query_as( "SELECT kind, search_tsv @@ plainto_tsquery('simple', 'needle') \ FROM events ORDER BY kind", ) .fetch_all(&pool) .await - .expect("read post-upgrade search behavior"); - assert_eq!(after, vec![(1, Some(true)), (30_179, None), (30_350, None)]); + .expect("read post-push search behavior"); + assert_eq!(after, vec![(1, Some(true)), (30_350, None)]); } #[tokio::test] diff --git a/crates/buzz-search/tests/fts_integration.rs b/crates/buzz-search/tests/fts_integration.rs index 175a01aaaa..e7c196ee3e 100644 --- a/crates/buzz-search/tests/fts_integration.rs +++ b/crates/buzz-search/tests/fts_integration.rs @@ -28,8 +28,6 @@ const MIGRATION_0007_SQL: &str = include_str!("../../../migrations/0007_nip_rs_r const MIGRATION_0008_SQL: &str = include_str!("../../../migrations/0008_fresh_install_search_allowlist.sql"); const MIGRATION_0014_SQL: &str = include_str!("../../../migrations/0014_push_lease_fts.sql"); -const MIGRATION_0033_SQL: &str = - include_str!("../../../migrations/0033_private_managed_agent_fts.sql"); async fn setup() -> (PgPool, String) { let url = std::env::var("BUZZ_TEST_DATABASE_URL").unwrap_or_else(|_| TEST_DB_URL.to_string()); @@ -83,9 +81,6 @@ async fn setup() -> (PgPool, String) { pool.execute(MIGRATION_0014_SQL) .await .expect("apply 0014 migration"); - pool.execute(MIGRATION_0033_SQL) - .await - .expect("apply 0033 migration"); (pool, schema) } diff --git a/migrations/0033_private_managed_agent_fts.sql b/migrations/0033_private_managed_agent_fts.sql deleted file mode 100644 index d2c2981396..0000000000 --- a/migrations/0033_private_managed_agent_fts.sql +++ /dev/null @@ -1,46 +0,0 @@ --- NIP-PMA kind:30179 carries the owner's private managed-agent payload --- (agent nsec, env vars, prompt) as NIP-44 ciphertext and is author-only. --- Exclude it from full-text search without changing the search policy of --- existing installations. Fresh installs are already safe via the positive --- allowlist from migration 0008; this closes the brownfield gap where a --- populated database still runs the legacy negative skip-set (0001/0005) --- and would tokenize the ciphertext into search_tsv. --- --- Same shape as 0014 (kind:30350): PostgreSQL cannot alter a generated --- expression in place, so capture the current expression, drop the column, --- and re-add it wrapped with the new exclusion. Every other kind keeps --- whatever policy the database had before. --- --- Operational cost: this is not free on large databases. DROP COLUMN + --- ADD ... GENERATED ... STORED rewrites the entire events heap and then --- rebuilds the GIN index, all under an ACCESS EXCLUSIVE lock inside the --- migration transaction (CREATE INDEX CONCURRENTLY is not possible --- here), with no lock_timeout. Expect relay downtime proportional to --- the size of events. The index is recreated from the stock definition --- below; any non-stock indexes or storage parameters on search_tsv are --- not captured or replayed. 0014 set this precedent on smaller tables; --- operators with large brownfield databases should schedule a window. -DO $$ -DECLARE - existing_expression TEXT; -BEGIN - SELECT pg_get_expr(d.adbin, d.adrelid) - INTO existing_expression - FROM pg_attrdef d - JOIN pg_attribute a - ON a.attrelid = d.adrelid - AND a.attnum = d.adnum - WHERE d.adrelid = 'events'::regclass - AND a.attname = 'search_tsv'; - - IF existing_expression IS NULL THEN - RAISE EXCEPTION 'events.search_tsv generated expression not found'; - END IF; - - ALTER TABLE events DROP COLUMN search_tsv; - EXECUTE format( - 'ALTER TABLE events ADD COLUMN search_tsv TSVECTOR GENERATED ALWAYS AS (CASE WHEN kind = 30179 THEN NULL::tsvector ELSE (%s) END) STORED', - existing_expression - ); - CREATE INDEX idx_events_search_tsv ON events USING GIN (search_tsv); -END $$; diff --git a/schema/schema.sql b/schema/schema.sql index c461039485..6e14e6be1b 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -219,9 +219,9 @@ CREATE TABLE events ( -- Privacy: encrypted/private routing wrappers and p-gated membership notices -- must never be discoverable through NIP-50 full-text search. NULL tsvector -- never matches `@@`. - -- Keep in sync with migrations (final state: 0001 + 0005 + 0014 + 0033). + -- Keep in sync with migrations (final state: 0001 + 0005 + 0009). search_tsv TSVECTOR GENERATED ALWAYS AS ( - CASE WHEN kind IN (1059, 30179, 30300, 30350, 30622, 44100, 44101, 44200) THEN NULL::tsvector + CASE WHEN kind IN (1059, 30300, 30350, 30622, 44100, 44101, 44200) THEN NULL::tsvector ELSE to_tsvector('simple', content) END ) STORED,