Files
gridline/desktop/src-tauri/src/db/introspection.rs
T
adrianbonpin 6854d889c5 refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via
  git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts)
- Root package.json becomes a private bun workspace container with
  orchestration scripts; desktop package renamed gridline-desktop
- Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin,
  static output, ready for Dokploy)
- Update release.yml for the new layout: projectPath: desktop,
  desktop/src-tauri resource paths, rust-cache workspace path
- Update README + AGENTS.md structure trees and dev commands
- Verified: vitest (1074), cargo test (354), desktop build, tauri dev
  (window launches), tauri build (dmg + app bundles), Astro build
2026-08-16 19:31:53 +08:00

890 lines
33 KiB
Rust

//! Schema introspection query builders.
//!
//! This module provides pure functions that generate SQL query strings
//! for database schema introspection. No actual DB connections are needed
//! for testing — all functions are deterministic string builders.
// ---------------------------------------------------------------------------
// PostgreSQL
// ---------------------------------------------------------------------------
/// Approximate row count for a table via `pg_class.reltuples`.
pub fn pg_reltuples_query(schema: &str, table: &str) -> String {
format!(
"SELECT reltuples::bigint AS count FROM pg_class \
WHERE relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '{}') \
AND relname = '{}'",
schema, table
)
}
/// List tables and views in a schema (or all non-system schemata).
///
/// When `schema` is `None` all schemata except the built-in system schemata
/// (`pg_catalog`, `information_schema`) are included.
pub fn pg_tables_query(schema: Option<&str>) -> String {
match schema {
Some(s) => format!(
"SELECT table_name, table_type FROM information_schema.tables \
WHERE table_schema = '{}' \
UNION ALL \
SELECT matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type \
FROM pg_matviews WHERE schemaname = '{}' \
ORDER BY table_name",
s, s
),
None => {
"SELECT table_name, table_type, table_schema FROM information_schema.tables \
WHERE table_schema NOT IN ('pg_catalog', 'information_schema') \
UNION ALL \
SELECT matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type, schemaname AS table_schema \
FROM pg_matviews WHERE schemaname NOT IN ('pg_catalog', 'information_schema') \
ORDER BY table_schema, table_name"
.to_string()
}
}
}
/// List all non-system schemata.
pub fn pg_schemas_query() -> String {
"SELECT schema_name FROM information_schema.schemata \
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') \
ORDER BY schema_name"
.to_string()
}
/// List non-template databases.
pub fn pg_databases_query() -> String {
"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname".to_string()
}
/// Column details with primary-key and foreign-key annotations.
///
/// Joins `information_schema.columns` with constraint metadata so that
/// each row includes PK / FK information when applicable.
pub fn pg_columns_query(schema: &str, table: &str) -> String {
format!(
r#"SELECT
c.column_name,
CASE WHEN c.data_type = 'USER-DEFINED' THEN c.udt_name ELSE c.data_type END AS data_type,
c.is_nullable,
c.character_maximum_length,
c.numeric_precision,
c.numeric_scale,
c.column_default,
c.ordinal_position,
pk.constraint_type,
fk.foreign_table_schema,
fk.foreign_table_name,
fk.foreign_column_name
FROM information_schema.columns c
LEFT JOIN (
SELECT kcu.column_name, kcu.table_schema, kcu.table_name, 'PRIMARY KEY' AS constraint_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_catalog = kcu.constraint_catalog
AND tc.constraint_schema = kcu.constraint_schema
AND tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
) pk ON c.table_schema = pk.table_schema AND c.table_name = pk.table_name AND c.column_name = pk.column_name
LEFT JOIN (
SELECT
kcu.column_name,
kcu.table_schema,
kcu.table_name,
ccu.table_schema AS foreign_table_schema,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_catalog = kcu.constraint_catalog
AND tc.constraint_schema = kcu.constraint_schema
AND tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON tc.constraint_catalog = ccu.constraint_catalog
AND tc.constraint_schema = ccu.constraint_schema
AND tc.constraint_name = ccu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
) fk ON c.table_schema = fk.table_schema AND c.table_name = fk.table_name AND c.column_name = fk.column_name
WHERE c.table_schema = '{}' AND c.table_name = '{}'
ORDER BY c.ordinal_position"#,
schema, table
)
}
// ---------------------------------------------------------------------------
// MySQL
// ---------------------------------------------------------------------------
/// List tables (and views) in the given schema.
///
/// When `schema` is `None` all non-system schemata are included (excluding
/// `information_schema`, `performance_schema`, `mysql`, and `sys`).
pub fn mysql_tables_query(schema: Option<&str>) -> String {
match schema {
Some(s) => format!(
"SELECT table_name, table_type FROM information_schema.tables \
WHERE table_schema = '{}' ORDER BY table_name",
s
),
None => {
"SELECT table_name, table_type, table_schema FROM information_schema.tables \
WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys') \
ORDER BY table_schema, table_name"
.to_string()
}
}
}
// ---------------------------------------------------------------------------
// SQLite
// ---------------------------------------------------------------------------
/// List tables and views from `sqlite_master`.
pub fn sqlite_tables_query() -> String {
"SELECT name AS table_name, type AS table_type FROM sqlite_master \
WHERE type IN ('table', 'view') ORDER BY name"
.to_string()
}
/// Column metadata via `PRAGMA table_info`.
pub fn sqlite_columns_query(table: &str) -> String {
format!("PRAGMA table_info('{}')", table)
}
/// Foreign-key metadata via `PRAGMA foreign_key_list`.
pub fn sqlite_foreign_keys_query(table: &str) -> String {
format!("PRAGMA foreign_key_list('{}')", table)
}
// ---------------------------------------------------------------------------
// Generic helpers
// ---------------------------------------------------------------------------
/// Build a paginated `SELECT` query.
///
/// Returns the SQL string (with `$1` / `$2` placeholders for `LIMIT` and
/// `OFFSET`) together with a vector of the corresponding `i64` parameter
/// values `[page_size, page * page_size]`.
///
/// When `columns` is empty the query uses `*`.
pub fn build_select_query(
schema: &str,
table: &str,
columns: &[String],
page: i64,
page_size: i64,
) -> (String, Vec<i64>) {
let cols = if columns.is_empty() {
"*".to_string()
} else {
let mut buf = String::new();
for (i, col) in columns.iter().enumerate() {
if i > 0 {
buf.push_str(", ");
}
buf.push('"');
buf.push_str(col);
buf.push('"');
}
buf
};
let sql = format!(
"SELECT {} FROM \"{}\".\"{}\" LIMIT $1 OFFSET $2",
cols, schema, table
);
let params = vec![page_size, page * page_size];
(sql, params)
}
/// Build a `COUNT(*)` query.
pub fn build_count_query(schema: &str, table: &str) -> String {
format!("SELECT COUNT(*) FROM \"{}\".\"{}\"", schema, table)
}
// ---------------------------------------------------------------------------
// Object introspection (functions, triggers, sequences, enums, extensions)
// ---------------------------------------------------------------------------
/// Query functions and procedures in a schema.
pub fn pg_functions_query(_schema: &str) -> String {
format!(
"SELECT p.proname, n.nspname, \
pg_catalog.format_type(p.prorettype, NULL) AS return_type, \
ARRAY(SELECT unnest(p.proargtypes::regtype[]::text[])) AS arg_types, \
ARRAY(SELECT unnest(p.proargnames::text[])) AS arg_names, \
ARRAY(SELECT unnest(p.proargmodes::text[])) AS arg_modes, \
l.lanname, pg_get_functiondef(p.oid) AS source, \
p.prokind::text \
FROM pg_proc p \
JOIN pg_namespace n ON p.pronamespace = n.oid \
JOIN pg_language l ON p.prolang = l.oid \
WHERE n.nspname = $1 \
AND p.prokind IN ('f', 'p') \
ORDER BY p.proname"
)
}
/// Query triggers in a schema.
pub fn pg_triggers_query(_schema: &str) -> String {
format!(
"SELECT t.tgname, tn.nspname AS trigger_schema, \
cn.nspname AS table_schema, c.relname AS table_name, \
CASE \
WHEN t.tgtype::int2 & 4 = 4 THEN 'INSERT' \
WHEN t.tgtype::int2 & 8 = 8 THEN 'DELETE' \
WHEN t.tgtype::int2 & 16 = 16 THEN 'UPDATE' \
WHEN t.tgtype::int2 & 32 = 32 THEN 'TRUNCATE' \
ELSE 'UNKNOWN' END AS event, \
CASE WHEN t.tgtype::int2 & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END AS timing, \
CASE WHEN t.tgtype::int2 & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END AS orientation, \
pg_get_triggerdef(t.oid) AS definition, \
t.tgenabled::text \
FROM pg_trigger t \
JOIN pg_class c ON t.tgrelid = c.oid \
JOIN pg_namespace cn ON c.relnamespace = cn.oid \
CROSS JOIN LATERAL (SELECT nspname FROM pg_namespace WHERE oid = (SELECT pronamespace FROM pg_proc WHERE oid = t.tgfoid)) tn \
WHERE cn.nspname = $1 AND NOT t.tgisinternal \
ORDER BY t.tgname"
)
}
/// Query sequences in a schema via information_schema.
pub fn pg_sequences_query(schema: &str) -> String {
format!(
"SELECT sequence_name, '{}' AS schema, \
COALESCE(start_value::text, '1'), \
COALESCE(minimum_value::text, '1'), \
COALESCE(maximum_value::text, '9223372036854775807'), \
COALESCE(increment::text, '1'), \
COALESCE(pg_catalog.pg_sequence_last_value(sequence_name::regclass)::text, '0'), \
COALESCE(cycle_option::text, 'NO') \
FROM information_schema.sequences \
WHERE sequence_schema = $1 \
ORDER BY sequence_name",
schema
)
}
/// Query enums in a schema.
pub fn pg_enums_query(_schema: &str) -> String {
format!(
"SELECT t.typname, n.nspname, \
ARRAY(SELECT e.enumlabel FROM pg_enum e \
WHERE e.enumtypid = t.oid ORDER BY e.enumsortorder) AS labels \
FROM pg_type t \
JOIN pg_namespace n ON t.typnamespace = n.oid \
WHERE t.typtype = 'e' AND n.nspname = $1 \
ORDER BY t.typname"
)
}
/// Query installed extensions.
pub fn pg_extensions_query() -> String {
"SELECT e.extname, n.nspname, e.extversion::text, \
pg_catalog.obj_description(e.oid, 'pg_extension') AS comment \
FROM pg_extension e \
JOIN pg_namespace n ON e.extnamespace = n.oid \
ORDER BY e.extname"
.to_string()
}
/// Query available (installable) extensions with default version + comment.
/// Schema-wide: `pg_available_extensions` is not schema-scoped.
pub fn pg_available_extensions_query() -> String {
"SELECT name, default_version::text AS version, comment \
FROM pg_available_extensions ORDER BY name"
.to_string()
}
/// Query indexes in a schema.
///
/// Returns index name, schema, table, definition (`pg_get_indexdef`),
/// uniqueness, access method, columns CSV, size in bytes, and tablespace.
pub fn pg_indexes_query(_schema: &str) -> String {
format!(
"SELECT \
i.relname AS index_name, \
ns.nspname AS schema, \
t.relname AS table_name, \
pg_get_indexdef(ix.indexrelid) AS definition, \
ix.indisunique AS is_unique, \
am.amname AS method, \
(SELECT string_agg(a.attname, ', ' ORDER BY ord.ord) \
FROM unnest(ix.indkey) WITH ORDINALITY AS ord(attnum, ord) \
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ord.attnum) AS columns, \
pg_relation_size(i.oid) AS size_bytes, \
ts.spcname AS tablespace \
FROM pg_index ix \
JOIN pg_class i ON i.oid = ix.indexrelid \
JOIN pg_class t ON t.oid = ix.indrelid \
JOIN pg_namespace ns ON t.relnamespace = ns.oid \
JOIN pg_am am ON i.relam = am.oid \
LEFT JOIN pg_tablespace ts ON i.reltablespace = ts.oid \
WHERE ns.nspname = $1 \
ORDER BY i.relname"
)
}
/// Query CHECK / UNIQUE / EXCLUSION constraints in a schema.
///
/// Primary and foreign keys are intentionally excluded — they surface in the
/// table grid. Returns name, schema, table, contype, definition
/// (`pg_get_constraintdef`), deferrability, validation, and columns CSV.
pub fn pg_constraints_query(_schema: &str) -> String {
format!(
"SELECT \
c.conname AS name, \
ns.nspname AS schema, \
cl.relname AS table_name, \
c.contype::text, \
pg_get_constraintdef(c.oid) AS definition, \
c.condeferrable, \
c.convalidated, \
(SELECT string_agg(a.attname, ', ' ORDER BY ord.ord) \
FROM unnest(c.conkey) WITH ORDINALITY AS ord(attnum, ord) \
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ord.attnum) AS columns \
FROM pg_constraint c \
JOIN pg_class cl ON c.conrelid = cl.oid \
JOIN pg_namespace ns ON cl.relnamespace = ns.oid \
WHERE ns.nspname = $1 AND c.contype IN ('c', 'u', 'x') \
ORDER BY c.conname"
)
}
// ---------------------------------------------------------------------------
// Roles, privileges, tablespaces, rebuild readiness
// ---------------------------------------------------------------------------
/// List non-system roles with all attributes used by the role form.
pub fn pg_roles_query() -> String {
"SELECT rolname, rolsuper, rolinherit, rolcreatedb, rolcreaterole, rolcanlogin, \
rolreplication, rolbypassrls, rolconnlimit::int8, COALESCE(rolvaliduntil::text, '') AS rolvaliduntil \
FROM pg_roles WHERE rolname !~ '^pg_' ORDER BY rolname"
.to_string()
}
/// All role-to-role memberships (member/admin/grantor).
pub fn pg_role_memberships_query() -> String {
"SELECT roleid::regrole::text AS role, member::regrole::text AS member, \
grantor::regrole::text AS grantor, admin_option \
FROM pg_auth_members ORDER BY role"
.to_string()
}
/// Table/view/matview privileges for a grantee, grouped one row per object.
pub fn pg_table_privileges_query(role: &str) -> String {
format!(
"SELECT table_schema AS schema, table_name AS name, \
array_agg(privilege_type::text) AS privileges, \
bool_or(is_grantable = 'YES') AS grantable \
FROM information_schema.table_privileges \
WHERE grantee = '{}' AND table_schema NOT IN ('pg_catalog','information_schema') \
GROUP BY table_schema, table_name \
ORDER BY table_schema, table_name",
role
)
}
/// Sequence privileges for a grantee, grouped one row per sequence.
/// aclexplode-based (role_sequence_grants was removed in PG 15).
pub fn pg_sequence_privileges_query(role: &str) -> String {
format!(
"SELECT n.nspname AS schema, c.relname AS name, \
array_agg(p.privilege_type::text) AS privileges, \
bool_or(p.is_grantable) AS grantable \
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace, \
LATERAL aclexplode(c.relacl) p \
WHERE c.relkind = 'S' AND n.nspname NOT IN ('pg_catalog','information_schema') \
AND p.grantee = (SELECT oid FROM pg_roles WHERE rolname = '{}') \
GROUP BY n.nspname, c.relname \
ORDER BY n.nspname, c.relname",
role
)
}
/// Routine (function/procedure) privileges for a grantee.
pub fn pg_routine_privileges_query(role: &str) -> String {
format!(
"SELECT routine_schema AS schema, routine_name AS name, \
array_agg(privilege_type::text) AS privileges, \
bool_or(is_grantable = 'YES') AS grantable \
FROM information_schema.routine_privileges \
WHERE grantee = '{}' \
GROUP BY routine_schema, routine_name \
ORDER BY routine_schema, routine_name",
role
)
}
/// Schema privileges for a grantee (USAGE/CREATE). aclexplode-based —
/// information_schema has no schema_privileges view.
pub fn pg_schema_privileges_query(role: &str) -> String {
format!(
"SELECT n.nspname AS name, \
array_agg(p.privilege_type::text) AS privileges, \
bool_or(p.is_grantable) AS grantable \
FROM pg_namespace n, LATERAL aclexplode(n.nspacl) p \
WHERE n.nspname NOT IN ('pg_catalog','information_schema') \
AND p.grantee = (SELECT oid FROM pg_roles WHERE rolname = '{}') \
GROUP BY n.nspname ORDER BY n.nspname",
role
)
}
/// Database privileges for a grantee via aclexplode (broadly compatible; avoids PG15-only view).
pub fn pg_database_privileges_query(role: &str) -> String {
format!(
"SELECT d.datname AS name, array_agg(a.privilege_type) AS privileges, \
bool_or(a.is_grantable) AS grantable \
FROM pg_database d, LATERAL aclexplode(d.datacl) a \
WHERE a.grantee = (SELECT oid FROM pg_roles WHERE rolname = '{}') \
AND d.datistemplate = false \
GROUP BY d.datname \
ORDER BY d.datname",
role
)
}
/// Non-system tablespaces for the table-options picker.
pub fn pg_tablespaces_query() -> String {
"SELECT spcname FROM pg_tablespace WHERE spcname !~ '^pg_' ORDER BY spcname".to_string()
}
/// Single query returning boolean blockers for a table rebuild (triggers, policies,
/// inheritance, partitioning, generated/identity columns). Parameterized via $1/$2.
pub fn pg_rebuild_readiness_query() -> String {
"SELECT \
EXISTS(SELECT 1 FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid \
JOIN pg_namespace n ON c.relnamespace = n.oid \
WHERE n.nspname = $1 AND c.relname = $2 AND NOT t.tgisinternal) AS has_triggers, \
EXISTS(SELECT 1 FROM pg_policy p JOIN pg_class c ON p.polrelid = c.oid \
JOIN pg_namespace n ON c.relnamespace = n.oid \
WHERE n.nspname = $1 AND c.relname = $2) AS has_policies, \
EXISTS(SELECT 1 FROM pg_inherits i JOIN pg_class c ON i.inhrelid = c.oid \
JOIN pg_namespace n ON c.relnamespace = n.oid \
WHERE n.nspname = $1 AND c.relname = $2) AS is_inherits, \
EXISTS(SELECT 1 FROM pg_partitioned_table pt JOIN pg_class c ON pt.partrelid = c.oid \
JOIN pg_namespace n ON c.relnamespace = n.oid \
WHERE n.nspname = $1 AND c.relname = $2) AS is_partitioned, \
EXISTS(SELECT 1 FROM information_schema.columns \
WHERE table_schema = $1 AND table_name = $2 AND is_generated <> '') AS has_generated"
.to_string()
}
/// FKs owned by this table (contype='f'). Parameterized $1 schema, $2 table.
pub fn pg_table_fk_out_query() -> String {
"SELECT c.conname, pg_get_constraintdef(c.oid) AS definition \
FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid \
JOIN pg_namespace n ON cl.relnamespace = n.oid \
WHERE n.nspname = $1 AND cl.relname = $2 AND c.contype = 'f'".to_string()
}
/// FKs from other tables referencing this table. Parameterized $1 schema, $2 table.
pub fn pg_table_fk_in_query() -> String {
"SELECT c.conname, cn.nspname AS own_schema, cl.relname AS own_table, \
pg_get_constraintdef(c.oid) AS definition \
FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid \
JOIN pg_namespace cn ON cl.relnamespace = cn.oid \
JOIN pg_class r ON c.confrelid = r.oid \
JOIN pg_namespace rn ON r.relnamespace = rn.oid \
WHERE rn.nspname = $1 AND r.relname = $2 AND c.contype = 'f'".to_string()
}
/// Grants on this table (all grantees), grouped. Parameterized $1 schema, $2 table.
pub fn pg_table_grants_query() -> String {
"SELECT grantee, array_agg(privilege_type::text) AS privileges, \
bool_or(is_grantable = 'YES') AS grantable \
FROM information_schema.table_privileges \
WHERE table_schema = $1 AND table_name = $2 AND grantee <> 'PUBLIC' \
GROUP BY grantee".to_string()
}
/// Sequences owned by this table's columns (via pg_depend). Parameterized $1 schema, $2 table.
pub fn pg_table_owned_sequences_query() -> String {
"SELECT sn.nspname AS seq_schema, s.relname AS seq_name, a.attname AS column \
FROM pg_depend d JOIN pg_class s ON d.objid = s.oid \
JOIN pg_namespace sn ON s.relnamespace = sn.oid \
JOIN pg_class t ON d.refobjid = t.oid \
JOIN pg_namespace tn ON t.relnamespace = tn.oid \
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid \
WHERE tn.nspname = $1 AND t.relname = $2 AND d.classid = 'pg_class'::regclass AND s.relkind = 'S'".to_string()
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// ---------------------------------------------------------------
// PostgreSQL
// ---------------------------------------------------------------
#[test]
fn pg_count_approximation_query_is_valid() {
let sql = pg_reltuples_query("public", "users");
assert!(
sql.contains("pg_class"),
"should query pg_class for row estimates; got: {}",
sql
);
assert!(sql.contains("public"), "should contain schema name");
assert!(sql.contains("users"), "should contain table name");
}
#[test]
fn pg_table_list_query_is_valid() {
let sql = pg_tables_query(Some("public"));
assert!(
sql.contains("information_schema.tables"),
"should query information_schema.tables; got: {}",
sql
);
assert!(sql.contains("public"), "should contain the given schema");
// Without schema filter — should exclude system schemata
let all_sql = pg_tables_query(None);
assert!(
all_sql.contains("information_schema.tables"),
"should query information_schema.tables"
);
assert!(
all_sql.contains("pg_catalog"),
"should exclude pg_catalog via NOT IN"
);
}
#[test]
fn pg_column_query_is_valid() {
let sql = pg_columns_query("public", "orders");
assert!(
sql.contains("information_schema.columns"),
"should query information_schema.columns; got: {}",
sql
);
assert!(sql.contains("public"), "should contain schema name");
assert!(sql.contains("orders"), "should contain table name");
assert!(
sql.contains("FOREIGN KEY"),
"should include FK constraint metadata"
);
assert!(
sql.contains("PRIMARY KEY"),
"should include PK constraint metadata"
);
}
#[test]
fn pg_schemas_query_is_valid() {
let sql = pg_schemas_query();
assert!(sql.contains("information_schema.schemata"));
assert!(sql.contains("pg_catalog"));
}
#[test]
fn pg_databases_query_is_valid() {
let sql = pg_databases_query();
assert!(sql.contains("pg_database"));
assert!(sql.contains("datistemplate"));
}
#[test]
fn pg_indexes_query_is_parameterized_and_joins() {
let sql = pg_indexes_query("public");
assert!(
sql.contains("$1"),
"schema must be parameterized; got: {}",
sql
);
assert!(
sql.contains("pg_indexes") || sql.contains("pg_index"),
"should query pg_index; got: {}",
sql
);
assert!(
sql.contains("pg_get_indexdef"),
"should include index definition"
);
assert!(sql.contains("indisunique"), "should include uniqueness");
}
#[test]
fn pg_constraints_query_filters_check_unique_exclusion() {
let sql = pg_constraints_query("public");
assert!(
sql.contains("$1"),
"schema must be parameterized; got: {}",
sql
);
assert!(
sql.contains("pg_constraint"),
"should query pg_constraint; got: {}",
sql
);
assert!(sql.contains("contype"), "should select contype");
assert!(sql.contains("'c'"), "should filter CHECK ('c')");
assert!(sql.contains("'u'"), "should filter UNIQUE ('u')");
assert!(sql.contains("'x'"), "should filter EXCLUSION ('x')");
assert!(
sql.contains("pg_get_constraintdef"),
"should include definition"
);
}
#[test]
fn pg_tables_query_includes_materialized_views() {
let sql = pg_tables_query(Some("public"));
assert!(
sql.contains("pg_matviews"),
"matview UNION must source pg_matviews; got: {}",
sql,
);
assert!(
sql.contains("MATERIALIZED VIEW"),
"should label materialized views"
);
}
// ---------------------------------------------------------------
// MySQL
// ---------------------------------------------------------------
#[test]
fn mysql_table_list_query_is_valid() {
let sql = mysql_tables_query(Some("mydb"));
assert!(
sql.contains("information_schema.tables"),
"should query information_schema.tables; got: {}",
sql
);
assert!(sql.contains("mydb"), "should contain the given schema");
let all_sql = mysql_tables_query(None);
assert!(all_sql.contains("information_schema.tables"));
assert!(all_sql.contains("performance_schema"));
}
// ---------------------------------------------------------------
// SQLite
// ---------------------------------------------------------------
#[test]
fn sqlite_table_list_query_is_valid() {
let sql = sqlite_tables_query();
assert!(
sql.contains("sqlite_master"),
"should query sqlite_master; got: {}",
sql
);
}
#[test]
fn sqlite_columns_query_is_valid() {
let sql = sqlite_columns_query("users");
assert!(
sql.contains("PRAGMA table_info"),
"should use PRAGMA table_info; got: {}",
sql
);
assert!(sql.contains("users"), "should contain table name");
}
#[test]
fn sqlite_foreign_keys_query_is_valid() {
let sql = sqlite_foreign_keys_query("orders");
assert!(
sql.contains("PRAGMA foreign_key_list"),
"should use PRAGMA foreign_key_list; got: {}",
sql
);
assert!(sql.contains("orders"), "should contain table name");
}
// ---------------------------------------------------------------
// Generic helpers
// ---------------------------------------------------------------
#[test]
fn build_paginated_query_with_limits() {
let columns = vec!["id".to_string(), "name".to_string()];
let (sql, params) = build_select_query("public", "users", &columns, 2, 25);
assert!(
sql.contains("LIMIT"),
"should contain LIMIT clause; got: {}",
sql
);
assert!(
sql.contains("OFFSET"),
"should contain OFFSET clause; got: {}",
sql
);
assert!(sql.contains("public"), "should contain schema name");
assert!(sql.contains("users"), "should contain table name");
assert!(sql.contains("\"id\""), "should quote column names");
assert!(sql.contains("\"name\""), "should quote column names");
// page=2, page_size=25 => offset = 50
assert_eq!(params, vec![25, 50], "params should be [page_size, offset]");
}
#[test]
fn build_paginated_query_empty_columns_uses_star() {
let (sql, _) = build_select_query("public", "users", &[], 0, 10);
assert!(
sql.contains('*'),
"empty columns should produce SELECT *; got: {}",
sql
);
}
#[test]
fn build_count_query_is_valid() {
let sql = build_count_query("public", "orders");
assert!(
sql.contains("COUNT(*)"),
"should contain COUNT(*); got: {}",
sql
);
assert!(sql.contains("public"), "should contain schema name");
assert!(sql.contains("orders"), "should contain table name");
}
// ---------------------------------------------------------------
// Object introspection
// ---------------------------------------------------------------
#[test]
fn pg_functions_query_has_expected_columns() {
let sql = pg_functions_query("public");
assert!(sql.contains("pg_proc"));
assert!(sql.contains("proname"));
}
#[test]
fn pg_triggers_query_has_expected_columns() {
let sql = pg_triggers_query("public");
assert!(sql.contains("pg_trigger"));
assert!(sql.contains("tgname"));
}
#[test]
fn pg_sequences_query_filters_by_schema() {
let sql = pg_sequences_query("myschema");
assert!(sql.contains("myschema"));
}
#[test]
fn pg_enums_query_has_typtype_e() {
let sql = pg_enums_query("public");
assert!(sql.contains("typtype = 'e'"));
}
#[test]
fn pg_extensions_query_selects_from_pg_extension() {
let sql = pg_extensions_query();
assert!(sql.contains("pg_extension"));
}
#[test]
fn pg_available_extensions_query_has_name_default_version_comment() {
let sql = pg_available_extensions_query();
assert!(sql.contains("pg_available_extensions"));
assert!(sql.contains("default_version"), "should select default version; got: {sql}");
assert!(sql.contains("comment"), "should select comment; got: {sql}");
assert!(sql.contains("ORDER BY name"), "should order by name; got: {sql}");
}
// ---------------------------------------------------------------
// Roles, privileges, tablespaces, rebuild readiness
// ---------------------------------------------------------------
#[test]
fn pg_roles_query_filters_pg_prefix() {
let sql = pg_roles_query();
assert!(sql.contains("pg_roles"));
assert!(sql.contains("rolname !~ '^pg_'"));
assert!(sql.contains("rolcanlogin"));
assert!(sql.contains("rolconnlimit::int8"));
}
#[test]
fn pg_role_memberships_query_uses_pg_auth_members() {
let sql = pg_role_memberships_query();
assert!(sql.contains("pg_auth_members"));
assert!(sql.contains("admin_option"));
}
#[test]
fn pg_table_privileges_query_filters_grantee_and_aggregates() {
let sql = pg_table_privileges_query("appuser");
assert!(sql.contains("information_schema.table_privileges"));
assert!(sql.contains("grantee = 'appuser'"));
assert!(sql.contains("array_agg"));
assert!(sql.contains("GROUP BY"));
}
#[test]
fn pg_database_privileges_query_uses_aclexplode() {
let sql = pg_database_privileges_query("appuser");
assert!(sql.contains("aclexplode"));
assert!(sql.contains("pg_database"));
assert!(sql.contains("appuser"));
}
#[test]
fn pg_rebuild_readiness_query_checks_blockers() {
let sql = pg_rebuild_readiness_query();
assert!(sql.contains("pg_trigger"));
assert!(sql.contains("pg_policy"));
assert!(sql.contains("pg_inherits"));
assert!(sql.contains("pg_partitioned_table"));
assert!(sql.contains("is_generated"));
}
#[test]
fn pg_tablespaces_query_filters_pg_prefix() {
let sql = pg_tablespaces_query();
assert!(sql.contains("pg_tablespace"));
assert!(sql.contains("spcname !~ '^pg_'"));
}
#[test]
fn pg_table_fk_out_query_uses_pg_constraint_f() {
let sql = pg_table_fk_out_query();
assert!(sql.contains("pg_constraint"));
assert!(sql.contains("contype = 'f'"));
assert!(sql.contains("$1") && sql.contains("$2"));
}
#[test]
fn pg_table_fk_in_query_finds_referencing_tables() {
let sql = pg_table_fk_in_query();
assert!(sql.contains("pg_constraint"));
assert!(sql.contains("confrel"));
assert!(sql.contains("$1") && sql.contains("$2"));
}
#[test]
fn pg_table_grants_query_uses_table_privileges() {
let sql = pg_table_grants_query();
assert!(sql.contains("information_schema.table_privileges"));
assert!(sql.contains("$1") && sql.contains("$2"));
assert!(sql.contains("array_agg"));
}
#[test]
fn pg_table_owned_sequences_query_uses_pg_depend() {
let sql = pg_table_owned_sequences_query();
assert!(sql.contains("pg_depend"));
assert!(sql.contains("pg_class"));
assert!(sql.contains("$1") && sql.contains("$2"));
}
}