rok-sqlc 0.6.0

Generate SQLx Rust database modules from SQL schema files
Documentation
//! Statements the SQL parser cannot read, recognised from their tokens.

use super::*;

/// `(column, Rust type, has a default, generated)` of each column.
fn shapes(table: &Table) -> Vec<(&str, &str, bool, bool)> {
    table
        .columns
        .iter()
        .map(|column| {
            (
                column.sql_name.as_str(),
                column.rust_type.as_str(),
                column.has_default,
                column.is_generated,
            )
        })
        .collect()
}

#[test]
fn alter_column_forms_that_leave_the_model_unchanged_are_dropped() {
    let table = one_table(
        "CREATE TABLE notes (
             id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
             body TEXT NOT NULL
         );
         ALTER TABLE notes ALTER COLUMN body SET STATISTICS 100;
         ALTER TABLE notes ALTER body SET STORAGE EXTERNAL, ALTER COLUMN body SET COMPRESSION lz4;
         ALTER TABLE notes ALTER COLUMN body SET (n_distinct = 100), ALTER COLUMN body RESET (n_distinct);
         ALTER TABLE notes ALTER COLUMN id SET GENERATED ALWAYS RESTART WITH 100;
         ALTER TABLE notes ALTER COLUMN id RESTART, ALTER COLUMN id SET INCREMENT BY 2;
         ALTER TABLE ONLY notes RESET (fillfactor), SET (fillfactor = 70), CLUSTER ON notes_pkey;
         ALTER TABLE notes SET WITHOUT CLUSTER, SET TABLESPACE fast, SET LOGGED, SET UNLOGGED;
         ALTER TABLE notes FORCE ROW LEVEL SECURITY, NO FORCE ROW LEVEL SECURITY;
         ALTER TABLE notes REPLICA IDENTITY FULL, OWNER TO app, VALIDATE CONSTRAINT notes_pkey;
         ALTER TABLE notes DISABLE TRIGGER ALL, ENABLE TRIGGER ALL;
         ALTER TABLE IF EXISTS notes ALTER CONSTRAINT notes_fk DEFERRABLE;",
    );
    assert_eq!(
        shapes(&table),
        [("id", "i64", true, true), ("body", "String", false, false),]
    );
}

/// `DROP IDENTITY` and `DROP EXPRESSION` hand the value to the caller, as
/// `DROP DEFAULT` does; a mixed statement applies its other clauses.
#[test]
fn drop_identity_and_drop_expression_make_the_column_a_plain_one() {
    let table = one_table(
        "CREATE TABLE notes (
             id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
             body TEXT NOT NULL,
             upper_body TEXT GENERATED ALWAYS AS (upper(body)) STORED
         );
         ALTER TABLE notes ALTER COLUMN id DROP IDENTITY IF EXISTS, ADD COLUMN title TEXT;
         ALTER TABLE notes ALTER COLUMN upper_body DROP EXPRESSION;",
    );
    assert_eq!(
        shapes(&table),
        [
            ("id", "i64", false, false),
            ("body", "String", false, false),
            ("upper_body", "Option<String>", false, false),
            ("title", "Option<String>", false, false),
        ]
    );
}

#[test]
fn alter_column_type_with_a_collation_changes_the_type() {
    let table = one_table(
        "CREATE TABLE notes (id BIGINT PRIMARY KEY, code INTEGER, body VARCHAR(10));
         ALTER TABLE notes ALTER COLUMN code TYPE TEXT COLLATE \"C\" USING code::text;
         ALTER TABLE notes ALTER COLUMN body SET DATA TYPE TEXT COLLATE pg_catalog.\"default\";",
    );
    assert_eq!(
        shapes(&table)[1..],
        [
            ("code", "Option<String>", false, false),
            ("body", "Option<String>", false, false),
        ]
    );
}

/// A clause neither the parser nor the recovery reads still fails the
/// table, at the statement's line, even next to clauses that are read.
#[test]
fn an_unknown_clause_next_to_a_recognised_one_still_fails_the_table() {
    let parsed = parse_files(&[
        (
            "0001.up.sql",
            "CREATE TABLE notes (id BIGINT PRIMARY KEY, body TEXT);",
        ),
        (
            "0002.up.sql",
            "-- move\nALTER TABLE notes ALTER COLUMN body SET STATISTICS 10, SET SCHEMA archive;",
        ),
    ]);
    assert!(parsed.schema.tables.is_empty());
    assert_eq!(parsed.failures.len(), 1);
    assert_eq!(parsed.failures[0].line, Some(2));
    assert!(
        parsed.failures[0]
            .reason
            .starts_with("the SQL parser cannot read this ALTER TABLE"),
        "{}",
        parsed.failures[0].reason
    );
}

#[test]
fn begin_atomic_bodies_and_other_function_like_statements_are_skipped() {
    let parsed = parse(
        "CREATE FUNCTION add_one(n INTEGER) RETURNS INTEGER LANGUAGE sql
         BEGIN ATOMIC
             SELECT CASE WHEN n IS NULL THEN 0 ELSE n END + 1;
             SELECT 1;
         END;
         CREATE OR REPLACE PROCEDURE touch() LANGUAGE sql BEGIN ATOMIC SELECT 1; END;
         DO $$ BEGIN RAISE NOTICE 'hi'; END $$;
         ALTER SEQUENCE notes_seq OWNED BY NONE;
         ALTER VIEW recent_notes ALTER COLUMN id SET DEFAULT 1;
         CREATE TABLE notes (id BIGINT PRIMARY KEY);",
    );
    assert!(parsed.failures.is_empty(), "{:?}", parsed.failures);
    assert_eq!(parsed.schema.tables.len(), 1);
}

#[test]
fn an_unreadable_create_table_still_stops_the_run_at_its_line() {
    let error = parse_schema(
        "CREATE FUNCTION f() RETURNS INTEGER LANGUAGE sql BEGIN ATOMIC SELECT 1; END;\n\
         CREATE TABEL notes (id BIGINT);",
    )
    .err()
    .expect("the run stops");
    assert!(
        format!("{error:#}").starts_with("line 2: invalid PostgreSQL SQL syntax"),
        "{error:#}"
    );
}

fn constraint_names(table: &Table) -> Vec<&str> {
    table
        .key_constraints
        .iter()
        .map(|constraint| constraint.name.as_str())
        .collect()
}

/// A renamed relation frees its name for the default names chosen later.
#[test]
fn renames_free_names_for_default_constraint_names() {
    let parsed = parse(
        "CREATE TABLE t (id BIGINT PRIMARY KEY, a TEXT, b TEXT, c TEXT, d TEXT);
         CREATE INDEX t_a_key ON t (id);
         CREATE SEQUENCE t_b_key;
         CREATE VIEW t_c_key AS SELECT 1;
         CREATE MATERIALIZED VIEW t_d_key AS SELECT 1;
         ALTER INDEX t_a_key RENAME TO t_a_idx;
         ALTER SEQUENCE IF EXISTS t_b_key RENAME TO t_b_seq;
         ALTER VIEW t_c_key RENAME TO t_c_view;
         ALTER MATERIALIZED VIEW t_d_key RENAME TO t_d_view;
         ALTER TABLE t ADD UNIQUE (a), ADD UNIQUE (b), ADD UNIQUE (c), ADD UNIQUE (d);",
    );
    assert!(parsed.failures.is_empty(), "{:?}", parsed.failures);
    assert_eq!(
        constraint_names(&parsed.schema.tables[0]),
        ["t_pkey", "t_a_key", "t_b_key", "t_c_key", "t_d_key"]
    );
}

/// Renaming a key constraint's index, or the constraint, renames it; the
/// old name is free again.
#[test]
fn renaming_a_key_constraint_or_its_index_renames_the_constraint() {
    let table = one_table(
        "CREATE TABLE t (id BIGINT PRIMARY KEY, a TEXT UNIQUE, b TEXT);
         ALTER INDEX t_pkey RENAME TO t_primary;
         ALTER TABLE t RENAME CONSTRAINT t_a_key TO t_a_unique;
         ALTER TABLE t RENAME CONSTRAINT some_check TO other_check;
         ALTER INDEX IF EXISTS missing RENAME TO still_missing;
         ALTER TABLE t ADD UNIQUE (a, b), ADD UNIQUE (a);",
    );
    assert_eq!(
        constraint_names(&table),
        ["t_primary", "t_a_unique", "t_a_b_key", "t_a_key"]
    );
}