use super::{ParseError, parse_sql_raw as parse_sql};
use crate::states::{Constraint, Schema, Volatility};
fn table<'a>(schema: &'a Schema, name: &str) -> &'a crate::states::Table {
schema
.tables
.get(name)
.unwrap_or_else(|| panic!("table '{name}' not found"))
}
fn col<'a>(schema: &'a Schema, tbl: &str, col: &str) -> &'a crate::states::Column {
let t = table(schema, tbl);
t.columns
.iter()
.find(|c| c.name == col)
.unwrap_or_else(|| panic!("column '{col}' not found on table '{tbl}'"))
}
#[test]
fn test_simple_table() {
let sql = "CREATE TABLE users (id bigserial PRIMARY KEY, name text NOT NULL);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.tables.contains_key("users"));
let id = col(&schema, "users", "id");
assert!(id.primary_key);
assert!(!id.nullable);
let name = col(&schema, "users", "name");
assert_eq!(name.col_type, "text");
assert!(!name.nullable);
}
#[test]
fn test_column_nullability() {
let sql = "CREATE TABLE t (
a integer NOT NULL,
b integer NULL,
c integer
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(!col(&schema, "t", "a").nullable);
assert!(col(&schema, "t", "b").nullable);
assert!(col(&schema, "t", "c").nullable);
}
#[test]
fn test_column_default() {
let sql = "CREATE TABLE t (created_at timestamptz NOT NULL DEFAULT now());";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let c = col(&schema, "t", "created_at");
assert_eq!(c.default.as_deref(), Some("now()"));
}
#[test]
fn test_table_level_primary_key() {
let sql = "CREATE TABLE orders (
id bigint,
PRIMARY KEY (id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let table = table(&schema, "orders");
let pk = table.primary_key.as_ref().expect("primary key");
assert_eq!(pk.name, "orders_pkey");
assert_eq!(pk.columns, ["id"]);
assert!(col(&schema, "orders", "id").primary_key);
}
#[test]
fn test_named_composite_primary_key_preserves_order() {
let sql = "CREATE TABLE order_lines (
order_id bigint,
tenant_id bigint,
CONSTRAINT order_lines_identity PRIMARY KEY (tenant_id, order_id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let table = table(&schema, "order_lines");
let pk = table.primary_key.as_ref().expect("primary key");
assert_eq!(pk.name, "order_lines_identity");
assert_eq!(pk.columns, ["tenant_id", "order_id"]);
assert_eq!(table.primary_key_column_names(), ["tenant_id", "order_id"]);
assert!(col(&schema, "order_lines", "tenant_id").primary_key);
assert!(col(&schema, "order_lines", "order_id").primary_key);
}
#[test]
fn test_column_level_foreign_key() {
let sql = "CREATE TABLE posts (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "posts");
assert_eq!(t.foreign_keys.len(), 1);
let fk = &t.foreign_keys[0];
assert_eq!(fk.columns, ["user_id"]);
assert_eq!(fk.to_table, "users");
assert_eq!(fk.to_columns, ["id"]);
assert_eq!(fk.name, "posts_user_id_fkey");
}
#[test]
fn test_table_level_foreign_key_named() {
let sql = "CREATE TABLE posts (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL,
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "posts");
assert_eq!(t.foreign_keys.len(), 1);
assert_eq!(t.foreign_keys[0].name, "fk_posts_user");
}
#[test]
fn test_named_composite_foreign_key_preserves_order() {
let sql = "CREATE TABLE orders (
tenant_id bigint,
user_id bigint,
CONSTRAINT orders_user_fkey FOREIGN KEY (tenant_id, user_id) REFERENCES users(tenant_id, id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let fk = &table(&schema, "orders").foreign_keys[0];
assert_eq!(fk.name, "orders_user_fkey");
assert_eq!(fk.columns, ["tenant_id", "user_id"]);
assert_eq!(fk.to_table, "users");
assert_eq!(fk.to_columns, ["tenant_id", "id"]);
}
#[test]
fn test_unnamed_composite_foreign_key_uses_deterministic_name() {
let sql = "CREATE TABLE orders (
tenant_id bigint,
user_id bigint,
FOREIGN KEY (tenant_id, user_id) REFERENCES users(tenant_id, id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let fk = &table(&schema, "orders").foreign_keys[0];
assert_eq!(fk.name, "orders_tenant_id_user_id_fkey");
assert_eq!(fk.columns, ["tenant_id", "user_id"]);
assert_eq!(fk.to_columns, ["tenant_id", "id"]);
}
#[test]
fn test_composite_foreign_key_preserves_schema_qualified_target() {
let sql = "CREATE TABLE orders (
tenant_id bigint,
user_id bigint,
FOREIGN KEY (tenant_id, user_id) REFERENCES auth.users(tenant_id, id)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let fk = &table(&schema, "orders").foreign_keys[0];
assert_eq!(fk.to_table, "auth.users");
assert_eq!(fk.to_columns, ["tenant_id", "id"]);
}
#[test]
fn test_table_level_unique_constraint() {
let sql = "CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL,
CONSTRAINT users_email_unique UNIQUE (email)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "users");
let unique = t
.constraints
.iter()
.find(|c| c.name() == "users_email_unique")
.expect("unique constraint not found");
assert!(matches!(unique, Constraint::Unique { columns, .. } if columns == &["email"]));
}
#[test]
fn test_table_level_check_constraint() {
let sql = "CREATE TABLE products (
id bigserial PRIMARY KEY,
price numeric NOT NULL,
CONSTRAINT products_price_positive CHECK (price > 0)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "products");
let chk = t
.constraints
.iter()
.find(|c| c.name() == "products_price_positive")
.expect("check constraint not found");
assert!(matches!(chk, Constraint::Check { expression, .. } if expression.contains("price")));
}
#[test]
fn test_column_level_check_constraint() {
let sql = "CREATE TABLE products (
price numeric CHECK (price > 0)
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "products");
assert_eq!(t.constraints.len(), 1);
assert_eq!(t.constraints[0].name(), "products_price_check");
}
#[test]
fn test_mixed_nullability() {
let sql = "CREATE TABLE t (
a text NOT NULL,
b text NULL,
c text NOT NULL,
d text
);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(!col(&schema, "t", "a").nullable);
assert!(col(&schema, "t", "b").nullable);
assert!(!col(&schema, "t", "c").nullable);
assert!(col(&schema, "t", "d").nullable);
}
#[test]
fn test_create_index() {
let sql = "
CREATE TABLE articles (id bigserial PRIMARY KEY, title text);
CREATE INDEX articles_title_idx ON articles (title);
";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "articles");
assert_eq!(t.indexes.len(), 1);
assert_eq!(t.indexes[0].name, "articles_title_idx");
assert!(!t.indexes[0].unique);
assert_eq!(t.indexes[0].columns, ["title"]);
}
#[test]
fn test_create_unique_index() {
let sql = "
CREATE TABLE users (id bigserial PRIMARY KEY, email text);
CREATE UNIQUE INDEX users_email_idx ON users (email);
";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "users");
assert!(t.indexes[0].unique);
}
#[test]
fn test_partial_index_with_predicate() {
let sql = "
CREATE TABLE events (id bigserial PRIMARY KEY, active boolean);
CREATE INDEX events_active_idx ON events (id) WHERE active = true;
";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "events");
let idx = &t.indexes[0];
assert!(
idx.predicate.is_some(),
"expected a predicate on the partial index"
);
assert!(idx.predicate.as_deref().unwrap().contains("active"));
}
#[test]
fn test_index_unknown_table_is_error() {
let sql = "CREATE INDEX missing_idx ON ghost (id);";
let err = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap_err();
assert!(
matches!(err, ParseError::UnknownTable { .. }),
"expected UnknownTable error, got: {err}"
);
}
#[test]
fn test_create_view() {
let sql = "CREATE VIEW active_users AS SELECT id, name FROM users WHERE active = true;";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.views.contains_key("active_users"));
let v = &schema.views["active_users"];
assert_eq!(v.name, "active_users");
assert!(v.definition.contains("active"));
}
#[test]
fn test_create_extension() {
let sql = "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.extensions.contains_key("uuid-ossp"));
}
#[test]
fn test_create_enum() {
let sql = "CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.enums.contains_key("mood"));
let e = &schema.enums["mood"];
assert_eq!(e.values, ["happy", "sad", "neutral"]);
}
#[test]
fn test_create_function() {
let sql = r#"
CREATE FUNCTION add_numbers(a integer, b integer) RETURNS integer
LANGUAGE sql
AS $$ SELECT a + b; $$;
"#;
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.functions.contains_key("add_numbers"));
let f = &schema.functions["add_numbers"];
assert_eq!(f.language, "sql");
assert!(f.arguments.contains("integer"));
assert_eq!(f.returns, "integer");
}
#[test]
fn test_function_volatility() {
let sql = r#"
CREATE FUNCTION pure_fn() RETURNS integer LANGUAGE sql IMMUTABLE AS $$ SELECT 1; $$;
"#;
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let f = &schema.functions["pure_fn"];
assert_eq!(f.volatility, Volatility::Immutable);
}
#[test]
fn test_function_security_definer() {
let sql = r#"
CREATE FUNCTION privileged() RETURNS void LANGUAGE sql SECURITY DEFINER AS $$ SELECT 1; $$;
"#;
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let f = &schema.functions["privileged"];
assert!(f.security_definer);
}
#[test]
fn test_public_schema_is_none() {
let sql = "CREATE TABLE public.users (id bigserial PRIMARY KEY);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.tables.contains_key("users"), "expected key 'users'");
assert_eq!(schema.tables["users"].schema, None);
}
#[test]
fn test_custom_schema_is_preserved() {
let sql = "CREATE TABLE analytics.events (id bigserial PRIMARY KEY);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.tables.contains_key("analytics.events"));
assert_eq!(
schema.tables["analytics.events"].schema,
Some("analytics".to_string())
);
}
#[test]
fn test_multi_statement_sql() {
let sql = "
CREATE TABLE users (id bigserial PRIMARY KEY, name text NOT NULL);
CREATE TABLE posts (id bigserial PRIMARY KEY, user_id bigint NOT NULL);
CREATE INDEX posts_user_idx ON posts (user_id);
";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.tables.contains_key("users"));
assert!(schema.tables.contains_key("posts"));
assert_eq!(table(&schema, "posts").indexes.len(), 1);
}
#[test]
fn test_unsupported_statement_is_error() {
let sql = "INSERT INTO users (name) VALUES ('alice');";
let err = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap_err();
assert!(
matches!(err, ParseError::UnsupportedStatement { .. }),
"expected Unsupported error, got: {err}"
);
}
#[test]
fn test_parse_error_remaps_second_statement_location() {
let sql = "\
CREATE TABLE ok (id integer);
CREATE TABLE broken (id integer,);
";
let err = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap_err();
let ParseError::Parse {
line,
segment_ordinal,
segment_start_line,
..
} = err
else {
panic!("expected Parse error");
};
assert_eq!(segment_ordinal, Some(2));
assert_eq!(segment_start_line, Some(1));
assert_eq!(line, Some(2));
}
#[test]
fn test_parse_error_remaps_location_with_leading_comments() {
let sql = "\
-- metadata for next statement
CREATE TABLE broken (id integer,);
";
let err = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap_err();
let ParseError::Parse {
line,
segment_ordinal,
segment_start_line,
..
} = err
else {
panic!("expected Parse error");
};
assert_eq!(segment_ordinal, Some(1));
assert_eq!(segment_start_line, Some(1));
assert_eq!(line, Some(2));
}
#[test]
fn test_parse_error_remaps_final_statement_without_semicolon() {
let sql = "\
CREATE TABLE ok (id integer);
CREATE TABLE broken (
id integer,
name text,
)
";
let err = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap_err();
let ParseError::Parse {
line,
segment_ordinal,
segment_start_line,
..
} = err
else {
panic!("expected Parse error");
};
assert_eq!(segment_ordinal, Some(2));
assert_eq!(segment_start_line, Some(1));
assert_eq!(line, Some(5));
}
#[test]
fn test_duplicate_table_is_error() {
let sql = "
CREATE TABLE users (id bigserial PRIMARY KEY);
CREATE TABLE users (email text);
";
let err = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap_err();
assert!(
matches!(err, ParseError::DuplicateTable(ref name) if name == "users"),
"expected DuplicateTable error, got: {err}"
);
}
#[test]
fn test_index_auto_name() {
let sql = "
CREATE TABLE orders (id bigserial PRIMARY KEY, status text);
CREATE INDEX ON orders (status);
";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "orders");
assert_eq!(t.indexes.len(), 1);
assert_eq!(t.indexes[0].name, "orders_status_idx");
}
#[test]
fn test_column_level_unique_auto_name() {
let sql = "CREATE TABLE users (id bigserial PRIMARY KEY, email text UNIQUE);";
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let t = table(&schema, "users");
assert_eq!(t.constraints.len(), 1);
assert_eq!(t.constraints[0].name(), "users_email_key");
}
#[test]
fn test_function_argument_modes() {
let sql = r#"
CREATE FUNCTION compute(IN a integer, OUT b integer)
RETURNS integer LANGUAGE sql AS $$ SELECT a; $$;
"#;
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
let f = &schema.functions["compute"];
assert!(f.arguments.contains("IN"), "expected IN mode in arguments");
assert!(
f.arguments.contains("OUT"),
"expected OUT mode in arguments"
);
}
#[test]
fn test_postgres_segmented_function_trigger_body() {
let sql = r#"
CREATE TABLE users (id integer PRIMARY KEY);
CREATE FUNCTION audit_users() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO audit_log VALUES (NEW.id);
RETURN NEW;
END;
$$;
CREATE TRIGGER users_ai AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_users();
"#;
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.functions.contains_key("audit_users"));
assert_eq!(table(&schema, "users").triggers.len(), 1);
}
#[test]
fn test_sqlite_segmented_trigger_body() {
let sql = r#"
CREATE TABLE users (id integer, touched integer);
CREATE TRIGGER users_ai AFTER INSERT ON users
BEGIN
UPDATE users SET touched = 1 WHERE id = NEW.id;
UPDATE users SET touched = touched + 1 WHERE id = NEW.id;
END;
"#;
let schema = super::parse_sql(sql, crate::dialects::Dialect::Sqlite).unwrap();
assert_eq!(table(&schema, "users").triggers.len(), 1);
}
#[test]
fn test_segmented_final_statement_without_semicolon() {
let sql = r#"
CREATE TABLE users (id integer PRIMARY KEY);
CREATE TABLE posts (id integer PRIMARY KEY)
"#;
let schema = parse_sql(sql, crate::dialects::Dialect::Postgres).unwrap();
assert!(schema.tables.contains_key("users"));
assert!(schema.tables.contains_key("posts"));
}