Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ description. See [PR #803](https://github.com/block/buzz/pull/803).
4. **Worktrees: `cd` in the same command** — shell CWD doesn't persist between tool calls. Use `cd /path && cargo build` as one command.
5. **Desktop crate excluded from root workspace** — `cargo test` at repo root does NOT run desktop tests. Use `cargo test --manifest-path desktop/src-tauri/Cargo.toml` explicitly.
6. **React render perf: `React.memo` is all-or-nothing** — it only skips a re-render when *every* prop is reference-stable; one unstable prop (inline arrow/JSX, or a hook returning a fresh `{}`/`[]`/`Map` each render) defeats it. Two repeat offenders: (a) React Query results (`useMutation`/`useQuery`) are a **new object each render** — depend on the stable method (`mutation.mutateAsync`), not the object; (b) derived `Map`/array state that recomputes on a version bump — wrap in a content-equality ref cache (`shared/hooks/useStableReference.ts`). When chasing interaction lag, **measure with DevTools closed and no perf probes** (an open Web Inspector + per-keystroke `console.log` inflate the numbers), and isolate by removing one suspect at a time rather than guessing.
7. **`pgschema` omits seed DML and some storage parameters** — Fresh desired-state bootstraps use `./bin/pgschema apply`, which does not execute `INSERT` statements or preserve every table storage parameter from `schema/schema.sql`. Put each unsupported invariant in `scripts/reconcile-schema-after-pgschema.sql` as an idempotent convergence statement plus a live catalog or data assertion. Every `pgschema apply` caller must run that script. A string assertion against `schema.sql` alone does not prove the pgschema-created database has the intended state.
7. **`pgschema` omits seed DML and some storage parameters** — `schema/schema.sql` is an ordered `\i` manifest: `schema/tables/public/<table>.sql` holds only that table's `CREATE TABLE` and `CREATE INDEX` statements (one table per file, the shape SchemaBot reads), and types, functions, partitions, triggers, and seed rows live in the sibling `schema/` directories. Fresh desired-state bootstraps use `./bin/pgschema apply`, which does not execute `INSERT` statements or preserve every table storage parameter from `schema/schema.sql`. Put each unsupported invariant in `scripts/reconcile-schema-after-pgschema.sql` as an idempotent convergence statement plus a live catalog or data assertion. Every `pgschema apply` caller must run that script. A string assertion against `schema.sql` alone does not prove the pgschema-created database has the intended state.

---

Expand Down
4 changes: 3 additions & 1 deletion crates/buzz-db/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ shared tables, so tests may run concurrently without coordinating cleanup.

## Choose the schema intentionally

Most tests use the committed desired-state schema from `schema/schema.sql`.
Most tests use the committed desired-state schema from `schema/schema.sql`,
a manifest that includes one `schema/tables/public/<table>.sql` per table plus
its types, functions, partitions, triggers, and seed rows.
That is the default and is appropriate for data-access behavior.

Tests in `migration::postgres_tests` receive an empty database and own the
Expand Down
219 changes: 184 additions & 35 deletions crates/buzz-db/src/runtime/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ mod postgres_tests {
assert!(!migrations[0].sql.as_str().contains("idx_events_tags_gin"));
// schema.sql (CI / isolated relay bootstrap) must carry the same index,
// or e-tag reads there run on plans prod never sees.
assert!(include_str!("../../../../schema/schema.sql").contains(
assert!(crate::test_support::desired_state_schema_sql().contains(
"CREATE INDEX idx_events_tags_gin ON events USING GIN (tags jsonb_path_ops)"
));

Expand Down Expand Up @@ -951,7 +951,7 @@ mod postgres_tests {
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")
assert!(crate::test_support::desired_state_schema_sql()
.contains("kind IN (1059, 30179, 30300, 30350, 30622, 44100, 44101, 44200)"));

// Public push-gateway authority is intentionally deployment-global and
Expand Down Expand Up @@ -1094,7 +1094,7 @@ mod postgres_tests {
.contains("CREATE INDEX relay_invites_expires_at_idx ON relay_invites (expires_at)"));
assert!(!relay_invites.contains("_operator_global_tables"));

let desired_schema = include_str!("../../../../schema/schema.sql");
let desired_schema = crate::test_support::desired_state_schema_sql();
assert!(
desired_schema.contains("CREATE TABLE join_policy_acceptances"),
"desired-state schema must include join-policy evidence used by invite claims",
Expand Down Expand Up @@ -1185,17 +1185,28 @@ mod postgres_tests {
// Fresh desired-state bootstrap must install the identical executable
// fence as migration 0032. CI and isolated relay startup use schema.sql
// without running migrations, so drift reopens rolling-deploy races.
fn extract_roster_fence(sql: &str) -> &str {
let fence_start = "CREATE OR REPLACE FUNCTION guard_channel_roster_snapshot()";
let fence_end = " FOR EACH ROW EXECUTE FUNCTION guard_channel_roster_snapshot();";
let start = sql.find(fence_start).expect("roster fence function");
let relative_end = sql[start..].find(fence_end).expect("roster fence trigger");
&sql[start..start + relative_end + fence_end.len()]
// The desired state keeps the function and its trigger in separate
// files, so compare each statement rather than one contiguous span.
fn extract_span<'a>(sql: &'a str, start: &str, end: &str) -> &'a str {
let begin = sql.find(start).expect("span start");
let relative_end = sql[begin..].find(end).expect("span end");
&sql[begin..begin + relative_end + end.len()]
}
for (start, end) in [
(
"CREATE OR REPLACE FUNCTION guard_channel_roster_snapshot()",
"$$ LANGUAGE plpgsql;",
),
(
"CREATE TRIGGER trg_events_guard_channel_roster_snapshot",
" FOR EACH ROW EXECUTE FUNCTION guard_channel_roster_snapshot();",
),
] {
assert_eq!(
extract_span(roster_fence, start, end),
extract_span(&desired_schema, start, end)
);
}
assert_eq!(
extract_roster_fence(roster_fence),
extract_roster_fence(desired_schema)
);

// The single-row heartbeat table is updated continuously. Prevent
// autovacuum from truncating its heap so standby queries are not
Expand Down Expand Up @@ -1469,7 +1480,162 @@ mod postgres_tests {
.sql
.as_str()
.contains("error_code"));
assert!(include_str!("../../../../schema/schema.sql").contains("error_code TEXT"));
assert!(
crate::test_support::desired_state_schema_sql().contains("error_code TEXT")
);
}

/// `schema/schema.sql` includes every desired-state file exactly once, and
/// `schema/tables/public/` (the SchemaBot schema directory's `public`
/// namespace) holds only one table per file: its CREATE TABLE plus the
/// CREATE INDEX statements on it. Types, functions, partitions, triggers
/// and seed rows belong in the sibling directories SchemaBot never reads.
#[test]
fn desired_state_schema_layout_is_complete_and_schemabot_shaped() {
use std::collections::BTreeSet;

fn sql_files(dir: &std::path::Path, root: &std::path::Path, out: &mut Vec<String>) {
for entry in std::fs::read_dir(dir).expect("read schema directory") {
let path = entry.expect("schema directory entry").path();
if path.is_dir() {
sql_files(&path, root, out);
} else if path.extension().is_some_and(|ext| ext == "sql") {
let relative = path.strip_prefix(root).expect("schema-relative path");
out.push(relative.to_str().expect("utf-8 path").replace('\\', "/"));
}
}
}

let schema_dir = crate::test_support::desired_state_schema_dir();
let manifest =
std::fs::read_to_string(schema_dir.join("schema.sql")).expect("read schema/schema.sql");
let mut included = Vec::new();
for line in manifest.lines() {
if let Some(include) = line.strip_prefix("\\i ") {
included.push(include.trim().to_owned());
}
}
let included_set: BTreeSet<_> = included.iter().cloned().collect();
assert_eq!(
included.len(),
included_set.len(),
"schema.sql includes a file twice"
);

let mut on_disk = Vec::new();
sql_files(&schema_dir, &schema_dir, &mut on_disk);
let on_disk: BTreeSet<_> = on_disk.into_iter().filter(|f| f != "schema.sql").collect();
assert_eq!(
on_disk, included_set,
"every schema/ file must be included by schema/schema.sql exactly once"
);

fn strip_comments(text: &str) -> String {
text.lines()
.map(|line| line.split_once("--").map_or(line, |(code, _)| code))
.collect::<Vec<_>>()
.join("\n")
}

// Table and index DDL must live in its table file, where SchemaBot
// reads it. A merge conflict resolved by pasting DDL into the manifest,
// or DDL in a sibling directory, would still build and pass CI.
for line in strip_comments(&manifest).lines().map(str::trim) {
assert!(
line.is_empty()
|| line.starts_with("\\i ")
|| line.to_ascii_uppercase().starts_with("CREATE EXTENSION "),
"schema/schema.sql may only hold \\i includes and CREATE EXTENSION; \
put table DDL in tables/public/<table>.sql: {line:.80}"
);
}
let non_table_files = std::iter::once("schema.sql").chain(
included
.iter()
.map(String::as_str)
.filter(|f| !f.starts_with("tables/public/")),
);
for file in non_table_files {
let text = std::fs::read_to_string(schema_dir.join(file)).expect("read schema file");
for statement in strip_comments(&text).to_ascii_uppercase().split(';') {
let tokens: Vec<_> = statement.split_whitespace().collect();
assert!(
!tokens.windows(2).any(|pair| matches!(
pair,
["CREATE" | "UNIQUE", "INDEX"] | ["DROP" | "ALTER", "TABLE" | "INDEX"]
)),
"{file} may not create, alter or drop tables or indexes; \
change tables/public/<table>.sql instead"
);
let creates_table = tokens.iter().enumerate().any(|(i, token)| {
*token == "CREATE"
&& tokens[i + 1..].iter().find(|t| {
!matches!(**t, "UNLOGGED" | "TEMP" | "TEMPORARY" | "GLOBAL" | "LOCAL")
}) == Some(&"TABLE")
});
assert!(
!creates_table
|| (file.starts_with("partitions/")
&& tokens.windows(2).any(|pair| pair == ["PARTITION", "OF"])),
"{file} may not CREATE TABLE; only partitions/ may, and only PARTITION OF. \
Declare tables in tables/public/<table>.sql"
);
}
}

let tables_dir = schema_dir.join("tables");
for entry in std::fs::read_dir(&tables_dir).expect("read schema/tables") {
let path = entry.expect("schema/tables entry").path();
assert!(
path.is_dir() && path.file_name().is_some_and(|name| name == "public"),
"schema/tables may contain only the `public` namespace: {}",
path.display()
);
}
let mut tables = 0;
for file in included.iter().filter(|f| f.starts_with("tables/public/")) {
let table = file
.strip_prefix("tables/public/")
.and_then(|name| name.strip_suffix(".sql"))
.expect("table file name");
let text = std::fs::read_to_string(schema_dir.join(file)).expect("read table file");
let code = strip_comments(&text);
let mut creates = 0;
for statement in code
.split(';')
.map(|s| s.split_whitespace().collect::<Vec<_>>().join(" "))
{
if statement.is_empty() {
continue;
}
if let Some(rest) = statement.strip_prefix("CREATE TABLE ") {
assert!(
rest.starts_with(&format!("{table} (")) && !rest.contains(" PARTITION OF "),
"{file} may only create table {table}: {statement:.80}"
);
creates += 1;
} else {
let index = statement
.strip_prefix("CREATE INDEX ")
.or_else(|| statement.strip_prefix("CREATE UNIQUE INDEX "))
.unwrap_or_else(|| {
panic!("{file} may only hold CREATE TABLE/INDEX: {statement:.80}")
});
assert!(
!index.starts_with("IF NOT EXISTS ")
&& !index.starts_with("CONCURRENTLY "),
"{file} must use a plain CREATE INDEX (SchemaBot refuses IF NOT EXISTS and CONCURRENTLY): {statement:.80}"
);
assert!(
index.contains(&format!(" ON {table} ")),
"{file} may only index {table}: {statement:.80}"
);
}
}
assert_eq!(creates, 1, "{file} must create exactly one table");
tables += 1;
}
assert!(tables > 0, "schema/tables/public declares no tables");
}

#[test]
Expand All @@ -1483,7 +1649,7 @@ mod postgres_tests {
assert!(sql.contains("NEW.kind IN (9, 40002, 45001, 45003)"));
assert!(!sql.contains("NEW.kind IN (7, 9, 1059, 40007, 46010)"));

let desired_schema = include_str!("../../../../schema/schema.sql");
let desired_schema = crate::test_support::desired_state_schema_sql();
assert!(desired_schema.contains("NEW.kind IN (9, 40002, 45001, 45003)"));
assert!(!desired_schema.contains("NEW.kind IN (7, 9, 1059, 40007, 46010)"));
}
Expand Down Expand Up @@ -1754,13 +1920,7 @@ mod postgres_tests {
.sql
.as_ref()
.to_ascii_lowercase();
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.expect("workspace root");
let schema = std::fs::read_to_string(workspace_root.join("schema/schema.sql"))
.expect("read schema/schema.sql")
.to_ascii_lowercase();
let schema = crate::test_support::desired_state_schema_sql().to_ascii_lowercase();

for sql in [&migration, &schema] {
assert!(sql.contains("approval_origin text not null default 'operator'"));
Expand All @@ -1782,13 +1942,7 @@ mod postgres_tests {
.sql
.as_ref()
.to_ascii_lowercase();
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.expect("workspace root");
let schema = std::fs::read_to_string(workspace_root.join("schema/schema.sql"))
.expect("read schema/schema.sql")
.to_ascii_lowercase();
let schema = crate::test_support::desired_state_schema_sql().to_ascii_lowercase();

for sql in [&migration, &schema] {
assert!(sql.contains("community_deletion_requests_owner_quota_reservations"));
Expand Down Expand Up @@ -1918,12 +2072,7 @@ mod postgres_tests {
.expect("embedded migration 0051")
.sql
.as_ref();
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.expect("workspace root");
let schema_sql = std::fs::read_to_string(workspace_root.join("schema/schema.sql"))
.expect("read schema/schema.sql");
let schema_sql = crate::test_support::desired_state_schema_sql();

let migration = surface(migration_0029);
let owner_admission_migration = surface(migration_0051);
Expand Down
10 changes: 6 additions & 4 deletions crates/buzz-db/src/store/channel_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3576,10 +3576,12 @@ mod postgres_tests {
.connect(&scratch_url)
.await
.expect("connect desired-schema scratch db");
sqlx::raw_sql(include_str!("../../../../schema/schema.sql"))
.execute(&pool)
.await
.expect("apply desired-state schema");
sqlx::raw_sql(sqlx::AssertSqlSafe(
crate::test_support::desired_state_schema_sql(),
))
.execute(&pool)
.await
.expect("apply desired-state schema");

let db = Db::from_pool(pool.clone());
let community_uuid = Uuid::new_v4();
Expand Down
5 changes: 1 addition & 4 deletions crates/buzz-db/src/store/deletion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8069,10 +8069,7 @@ mod postgres_tests {
let (base_prefix, _) = base_url.rsplit_once('/').expect("database url has a path");
let probe_url = format!("{base_prefix}/{probe_db}");

let schema_sql = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../schema/schema.sql"),
)
.expect("read schema/schema.sql");
let schema_sql = crate::test_support::desired_state_schema_sql();
let bootstrap = PgPool::connect(&probe_url)
.await
.expect("connect probe database");
Expand Down
31 changes: 31 additions & 0 deletions crates/buzz-db/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,34 @@ pub(crate) async fn quiesce_community_for_tests(
pub(crate) fn is_admission_rejection(error: &crate::DbError) -> bool {
matches!(error, crate::DbError::AccessDenied(message) if message.contains("write-fenced (quiescing)"))
}

/// The desired-state schema as one SQL script.
///
/// `schema/schema.sql` is a manifest of `\i` includes resolved relative to the
/// including file (as `pgschema` does). This expands every include in place,
/// so text assertions and `sqlx::raw_sql` bootstraps see the same statements,
/// in the same order, that `pgschema apply` loads.
pub(crate) fn desired_state_schema_sql() -> String {
fn expand(path: &std::path::Path, out: &mut String) {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
let dir = path.parent().expect("schema file has a parent directory");
for line in text.split_inclusive('\n') {
match line.trim_end().strip_prefix("\\i ") {
Some(include) => expand(&dir.join(include.trim()), out),
None => out.push_str(line),
}
}
if !out.ends_with('\n') {
out.push('\n');
}
}
let mut out = String::new();
expand(&desired_state_schema_dir().join("schema.sql"), &mut out);
out
}

/// `schema/` at the workspace root.
pub(crate) fn desired_state_schema_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../schema")
}
Loading
Loading