ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
use super::*;

fn fields(specs: &[&str]) -> Vec<Field> {
    parse_fields(&specs.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>()).unwrap()
}

#[test]
fn create_table_names() {
    let sql = infer("create_tags", &fields(&["label:string^"])).unwrap();
    assert!(
        sql.starts_with("CREATE TABLE tags (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    label TEXT NOT NULL,\n")
    );
    assert!(sql.ends_with("CREATE UNIQUE INDEX index_tags_on_label ON tags (label);\n"));
}

#[test]
fn add_columns_give_existing_rows_a_value() {
    let sql = infer(
        "add_details_to_posts",
        &fields(&["slug:string^", "views:integer", "note:text?", "live:boolean", "editor:references?"]),
    )
    .unwrap();
    assert_eq!(
        sql,
        "ALTER TABLE posts ADD COLUMN slug TEXT NOT NULL DEFAULT '';\n\
         ALTER TABLE posts ADD COLUMN views INTEGER NOT NULL DEFAULT 0;\n\
         ALTER TABLE posts ADD COLUMN note TEXT;\n\
         ALTER TABLE posts ADD COLUMN live INTEGER NOT NULL DEFAULT 0;\n\
         ALTER TABLE posts ADD COLUMN editor_id INTEGER REFERENCES editors(id) ON DELETE SET NULL;\n\
         CREATE UNIQUE INDEX index_posts_on_slug ON posts (slug);\n\
         CREATE INDEX index_posts_on_editor_id ON posts (editor_id);\n"
    );
    assert_eq!(infer("add_x_to_posts", &[]).unwrap_err().message, "add_..._to_... needs the columns to add");
    assert_eq!(
        infer("add_author_to_posts", &fields(&["author:references"])).unwrap_err().message,
        "`author_id` must be optional when added to an existing table"
    );
    let error = infer("add_cover_to_posts", &fields(&["cover:attachment"])).unwrap_err();
    assert_eq!(error.message, "`cover` must be optional when added to an existing table");
    assert_eq!(error.hint.as_deref(), Some("existing rows have no file: use `name:attachment?`"));
    assert_eq!(
        infer("add_cover_to_posts", &fields(&["cover:attachment?"])).unwrap(),
        "ALTER TABLE posts ADD COLUMN cover_key TEXT;\nALTER TABLE posts ADD COLUMN cover_filename TEXT;\n\
         ALTER TABLE posts ADD COLUMN cover_content_type TEXT;\nALTER TABLE posts ADD COLUMN cover_size INTEGER;\n"
    );
    assert_eq!(
        infer("add_settings_to_posts", &fields(&["settings:json", "extra:json?"])).unwrap(),
        "ALTER TABLE posts ADD COLUMN settings TEXT NOT NULL CHECK (json_valid(settings)) DEFAULT '{}';\n\
         ALTER TABLE posts ADD COLUMN extra TEXT CHECK (json_valid(extra));\n"
    );
}

#[test]
fn remove_columns_from_the_name_or_the_fields() {
    assert_eq!(
        infer("remove_slug_from_posts", &[]).unwrap(),
        "DROP INDEX IF EXISTS index_posts_on_slug;\nALTER TABLE posts DROP COLUMN slug;\n"
    );
    assert_eq!(
        infer("remove_old_from_posts", &fields(&["a:string^", "b:integer", "author:references?"])).unwrap(),
        "DROP INDEX IF EXISTS index_posts_on_a;\nDROP INDEX IF EXISTS index_posts_on_author_id;\n\
         ALTER TABLE posts DROP COLUMN a;\nALTER TABLE posts DROP COLUMN b;\nALTER TABLE posts DROP COLUMN author_id;\n"
    );
    assert_eq!(
        infer("remove_cover_from_posts", &fields(&["cover:attachment?"])).unwrap(),
        "ALTER TABLE posts DROP COLUMN cover_key;\nALTER TABLE posts DROP COLUMN cover_filename;\n\
         ALTER TABLE posts DROP COLUMN cover_content_type;\nALTER TABLE posts DROP COLUMN cover_size;\n"
    );
}

fn specs(names: &[&str]) -> Vec<String> {
    names.iter().map(|s| (*s).to_owned()).collect()
}

fn structural_sql(name: &str, columns: &[&str]) -> Result<Option<(String, bool)>, CliError> {
    structural(name, &specs(columns), &|| Ok(None))
}

#[test]
fn index_migrations_take_column_names() {
    assert_eq!(
        structural_sql("add_index_to_posts", &["author_id", "created_at"]).unwrap(),
        Some((
            "CREATE INDEX index_posts_on_author_id_and_created_at ON posts (author_id, created_at);\n".to_owned(),
            false
        ))
    );
    assert_eq!(
        structural_sql("add_unique_index_to_posts", &["slug"]).unwrap().unwrap().0,
        "CREATE UNIQUE INDEX index_posts_on_slug ON posts (slug);\n"
    );
    assert_eq!(
        structural_sql("remove_index_from_posts", &["author_id", "created_at"]).unwrap().unwrap().0,
        "DROP INDEX IF EXISTS index_posts_on_author_id_and_created_at;\n"
    );
    for columns in [&[][..], &["title:string"][..]] {
        let error = structural_sql("add_index_to_posts", columns).unwrap_err();
        assert_eq!(error.message, "`add_index_to_posts` needs the indexed column names");
    }
    assert_eq!(structural_sql("add_slug_to_posts", &["slug:string"]).unwrap(), None);
}

#[test]
fn renames_and_drops() {
    let sql = |name: &str| structural_sql(name, &[]).unwrap().unwrap();
    assert_eq!(
        sql("rename_title_to_headline_in_posts"),
        ("ALTER TABLE posts RENAME COLUMN title TO headline;\n".to_owned(), true)
    );
    assert_eq!(sql("rename_posts_to_articles").0, "ALTER TABLE posts RENAME TO articles;\n");
    assert_eq!(sql("drop_tags").0, "DROP TABLE tags;\n");
    let error = structural_sql("rename_posts", &[]).unwrap_err();
    assert_eq!(error.message, "cannot tell what `rename_posts` renames");
    let error = structural_sql("drop_tags", &["label:string"]).unwrap_err();
    assert_eq!(error.message, "`drop_tags` takes no fields");
}

const SCHEMA_SQL: &str = "-- Schema of the local database.\n\n\
CREATE TABLE authors (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    name TEXT NOT NULL\n);\n\n\
CREATE TABLE posts (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    title TEXT NOT NULL CHECK (length(title) > 0),\n    \
rating REAL DEFAULT (0.5),\n    note TEXT DEFAULT 'a, b',\n    author_id INTEGER REFERENCES authors(id) ON DELETE SET NULL,\n    \
UNIQUE (title, author_id)\n);\n\n\
CREATE INDEX index_posts_on_author_id ON posts (author_id);\n\n\
CREATE UNIQUE INDEX index_authors_on_name ON authors (name);\n";

#[test]
fn rebuilds_copy_the_current_definition() {
    let (sql, model) = structural("rebuild_posts", &[], &|| Ok(Some(SCHEMA_SQL.to_owned()))).unwrap().unwrap();
    assert!(model);
    assert_eq!(
        sql,
        "-- Rebuilds `posts` to change what ALTER TABLE cannot (a column's type, NOT NULL,\n\
         -- DEFAULT, CHECK or REFERENCES): edit the CREATE TABLE below, and keep both\n\
         -- column lists of the INSERT in step with it.\n\
         PRAGMA defer_foreign_keys = true;\n\
         CREATE TABLE posts_new (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    title TEXT NOT NULL CHECK (length(title) > 0),\n    \
         rating REAL DEFAULT (0.5),\n    note TEXT DEFAULT 'a, b',\n    author_id INTEGER REFERENCES authors(id) ON DELETE SET NULL,\n    \
         UNIQUE (title, author_id)\n);\n\
         INSERT INTO posts_new (id, title, rating, note, author_id) SELECT id, title, rating, note, author_id FROM posts;\n\
         DROP TABLE posts;\n\
         ALTER TABLE posts_new RENAME TO posts;\n\
         CREATE INDEX index_posts_on_author_id ON posts (author_id);\n\
         PRAGMA defer_foreign_keys = false;\n"
    );
}

#[test]
fn rebuilds_refuse_missing_schemas_tables_and_referenced_tables() {
    let error = structural("rebuild_posts", &[], &|| Ok(None)).unwrap_err();
    assert_eq!(error.message, "db/schema.sql not found");
    let error = structural("rebuild_tags", &[], &|| Ok(Some(SCHEMA_SQL.to_owned()))).unwrap_err();
    assert_eq!(error.message, "table `tags` is not in db/schema.sql");
    let error = structural("rebuild_authors", &[], &|| Ok(Some(SCHEMA_SQL.to_owned()))).unwrap_err();
    assert_eq!(error.message, "`posts` references `authors`: rebuilding it would delete or clear their rows");
}

#[test]
fn other_names_are_empty_unless_fields_are_given() {
    assert_eq!(infer("backfill_slugs", &[]).unwrap(), "");
    assert_eq!(
        infer("backfill_slugs", &fields(&["slug:string"])).unwrap_err().message,
        "cannot tell which table `backfill_slugs` changes"
    );
}

#[test]
fn added_enums_default_to_their_first_value() {
    assert_eq!(
        infer("add_status_to_posts", &fields(&["status:enum:draft,live", "mood:enum:ok?"])).unwrap(),
        "ALTER TABLE posts ADD COLUMN status TEXT NOT NULL CHECK (status IN ('draft', 'live')) DEFAULT 'draft';\n\
         ALTER TABLE posts ADD COLUMN mood TEXT CHECK (mood IN ('ok'));\n"
    );
}

#[test]
fn a_public_id_gives_every_existing_row_its_own_random_id() {
    let sql = infer("add_public_id_to_posts", &fields(&["public_id:token"])).unwrap();
    assert_eq!(
        sql,
        "ALTER TABLE posts ADD COLUMN public_id TEXT NOT NULL DEFAULT '';\n\
         UPDATE posts SET public_id = lower(hex(randomblob(16))) WHERE public_id = '';\n\
         CREATE UNIQUE INDEX index_posts_on_public_id ON posts (public_id);\n"
    );
}