use super::*;
#[test]
fn create_table_with_tensor_column() {
let stmt = first("CREATE TABLE docs (id INTEGER PRIMARY KEY, chunks TENSOR(4))");
let Statement::CreateTable(ct) = stmt else {
panic!("not CREATE TABLE");
};
assert!(matches!(ct.columns[1].ty, ColumnType::Tensor(4)));
}
#[test]
fn create_index_records_access_method() {
let stmt = first("CREATE INDEX idx_body ON docs USING gin (body)");
let Statement::CreateIndex(ci) = stmt else {
panic!("not CREATE INDEX");
};
assert_eq!(ci.table, "docs");
assert_eq!(ci.access_method, "gin");
assert_eq!(
ci.columns,
vec![crate::ast::IndexKey::Column("body".into())]
);
}
#[test]
fn table_commands_preserve_qualified_relation_names() {
let stmt = first("ALTER TABLE app.docs ADD COLUMN version INTEGER");
let Statement::AlterTable(alter) = stmt else {
panic!("not ALTER TABLE");
};
assert_eq!(alter.table, "app.docs");
let stmt = first("ALTER TABLE app.docs RENAME TO archived_docs");
let Statement::AlterTable(rename) = stmt else {
panic!("not ALTER TABLE RENAME");
};
assert_eq!(rename.table, "app.docs");
let Statement::Update(update) = first("UPDATE app.docs SET version = 2") else {
panic!("not UPDATE");
};
assert_eq!(update.table, "app.docs");
let Statement::Delete(delete) = first("DELETE FROM app.docs") else {
panic!("not DELETE");
};
assert_eq!(delete.table, "app.docs");
let Statement::Truncate {
tables,
restart_identity,
..
} = first("TRUNCATE app.docs")
else {
panic!("not TRUNCATE");
};
assert_eq!(tables.len(), 1);
assert_eq!(tables[0].table, "app.docs");
assert!(tables[0].include_descendants);
assert!(!restart_identity);
let Statement::Truncate { tables, .. } = first("TRUNCATE ONLY app.docs") else {
panic!("not TRUNCATE ONLY");
};
assert!(!tables[0].include_descendants);
let Statement::Truncate {
restart_identity, ..
} = first("TRUNCATE app.docs RESTART IDENTITY")
else {
panic!("not TRUNCATE RESTART IDENTITY");
};
assert!(restart_identity);
let Statement::Insert(insert) = first("INSERT INTO app.docs (version) VALUES (1)") else {
panic!("not INSERT");
};
assert_eq!(insert.table, "app.docs");
}
#[test]
fn alter_column_type_preserves_the_using_expression() {
let Statement::AlterTable(alter) =
first("ALTER TABLE metrics ALTER COLUMN value TYPE text USING (value + delta)::text")
else {
panic!("not ALTER TABLE ALTER COLUMN TYPE");
};
let [crate::ast::AlterTableAction::AlterColumnType { name, ty, using }] =
alter.actions.as_slice()
else {
panic!("not ALTER COLUMN TYPE");
};
assert_eq!(name, "value");
assert_eq!(*ty, ColumnType::Text);
assert!(matches!(using, Some(Expr::Cast { ty, .. }) if ty == "text"));
}
#[test]
fn insert_with_array_literal() {
let stmt = first(
"INSERT INTO docs (id, title, embedding) VALUES \
(1, 'rust language', ARRAY[0.1, 0.2, 0.3])",
);
let Statement::Insert(i) = stmt else {
panic!("not INSERT");
};
assert_eq!(i.table, "docs");
assert_eq!(i.columns, vec!["id", "title", "embedding"]);
assert_eq!(i.rows.len(), 1);
assert_eq!(i.rows[0].len(), 3);
match &i.rows[0][2] {
Expr::Array(v) => assert_eq!(v.len(), 3),
other => panic!("expected Array, got {other:?}"),
}
}
#[test]
fn insert_default_values_preserves_one_input_row() {
let Statement::Insert(insert) = first("INSERT INTO docs DEFAULT VALUES RETURNING id") else {
panic!("not INSERT");
};
assert!(insert.columns.is_empty());
assert_eq!(insert.rows, vec![Vec::new()]);
assert!(insert.select_source.is_none());
assert_eq!(insert.returning.len(), 1);
}
#[test]
fn insert_set_operation_is_compiled_as_one_select_source() {
let Statement::Insert(insert) =
first("INSERT INTO dst SELECT id FROM lhs UNION ALL SELECT id FROM rhs LIMIT 2 OFFSET 1")
else {
panic!("not INSERT");
};
assert!(insert.rows.is_empty());
let source = insert
.select_source
.expect("INSERT must retain its SELECT source");
let set_op = source
.set_op
.as_ref()
.expect("INSERT source must retain its set operation");
assert_eq!(set_op.kind, crate::ast::SetOpKind::Union);
assert!(set_op.all);
assert!(set_op.combined_limit.is_some());
assert!(set_op.combined_offset.is_some());
}
#[test]
fn merge_not_matched_by_source_preserves_all_actions() {
let Statement::Merge(merge) = first(
"MERGE INTO target USING source ON target.id = source.id \
WHEN NOT MATCHED BY SOURCE AND target.retired THEN UPDATE SET value = value + 1 \
WHEN NOT MATCHED BY SOURCE AND target.expired THEN DELETE \
WHEN NOT MATCHED BY SOURCE THEN DO NOTHING",
) else {
panic!("expected MERGE");
};
assert!(matches!(
merge.when_clauses.as_slice(),
[
crate::ast::MergeWhen::UpdateNotMatchedBySource { .. },
crate::ast::MergeWhen::DeleteNotMatchedBySource { .. },
crate::ast::MergeWhen::NothingNotMatchedBySource { .. }
]
));
}
#[test]
fn merge_preserves_only_target_scope() {
let Statement::Merge(merge) = first(
"MERGE INTO ONLY target USING source ON target.id = source.id \
WHEN MATCHED THEN DO NOTHING",
) else {
panic!("expected MERGE");
};
assert!(!merge.include_descendants);
let Statement::Merge(merge) = first(
"MERGE INTO target USING source ON target.id = source.id \
WHEN MATCHED THEN DO NOTHING",
) else {
panic!("expected MERGE");
};
assert!(merge.include_descendants);
}
#[test]
fn merge_rejects_a_clause_after_an_unconditional_clause_of_the_same_kind() {
let error = compile(
"MERGE INTO target USING source ON target.id = source.id \
WHEN NOT MATCHED BY SOURCE THEN DO NOTHING \
WHEN MATCHED THEN DO NOTHING \
WHEN NOT MATCHED BY SOURCE AND target.retired THEN DELETE",
)
.unwrap_err();
assert_eq!(error.sqlstate(), Some("42601"));
assert!(error.to_string().contains("unreachable WHEN clause"));
}