Fix: actually enforce uniqueness in CREATE UNIQUE INDEX

...we just didn't do it
This commit is contained in:
Jussi Saurio
2025-10-03 22:54:35 +03:00
parent cb96c3e944
commit 8dac1ba21a
3 changed files with 78 additions and 3 deletions

View File

@@ -1,11 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use crate::bail_parse_error; use crate::bail_parse_error;
use crate::error::SQLITE_CONSTRAINT_UNIQUE;
use crate::schema::{Table, RESERVED_TABLE_PREFIXES}; use crate::schema::{Table, RESERVED_TABLE_PREFIXES};
use crate::translate::emitter::{ use crate::translate::emitter::{
emit_cdc_full_record, emit_cdc_insns, prepare_cdc_if_necessary, OperationMode, Resolver, emit_cdc_full_record, emit_cdc_insns, prepare_cdc_if_necessary, OperationMode, Resolver,
}; };
use crate::translate::expr::{translate_condition_expr, ConditionMetadata}; use crate::translate::expr::{translate_condition_expr, ConditionMetadata};
use crate::translate::insert::format_unique_violation_desc;
use crate::translate::plan::{ use crate::translate::plan::{
ColumnUsedMask, IterationDirection, JoinedTable, Operation, Scan, TableReferences, ColumnUsedMask, IterationDirection, JoinedTable, Operation, Scan, TableReferences,
}; };
@@ -91,6 +93,7 @@ pub fn translate_create_index(
}; };
let original_columns = columns; let original_columns = columns;
let columns = resolve_sorted_columns(&tbl, columns)?; let columns = resolve_sorted_columns(&tbl, columns)?;
let unique = unique_if_not_exists.0;
let idx = Arc::new(Index { let idx = Arc::new(Index {
name: idx_name.clone(), name: idx_name.clone(),
@@ -106,7 +109,7 @@ pub fn translate_create_index(
default: col.default.clone(), default: col.default.clone(),
}) })
.collect(), .collect(),
unique: unique_if_not_exists.0, unique,
ephemeral: false, ephemeral: false,
has_rowid: tbl.has_rowid, has_rowid: tbl.has_rowid,
// store the *original* where clause, because we need to rewrite it // store the *original* where clause, because we need to rewrite it
@@ -295,8 +298,34 @@ pub fn translate_create_index(
cursor_id: sorter_cursor_id, cursor_id: sorter_cursor_id,
pc_if_empty: sorted_loop_end, pc_if_empty: sorted_loop_end,
}); });
program.preassign_label_to_next_insn(sorted_loop_start);
let sorted_record_reg = program.alloc_register(); let sorted_record_reg = program.alloc_register();
if unique {
// Since the records to be inserted are sorted, we can compare prev with current and if they are equal,
// we fall through to Halt with a unique constraint violation error.
let goto_label = program.allocate_label();
let label_after_sorter_compare = program.allocate_label();
program.resolve_label(goto_label, program.offset());
program.emit_insn(Insn::Goto {
target_pc: label_after_sorter_compare,
});
program.preassign_label_to_next_insn(sorted_loop_start);
program.emit_insn(Insn::SorterCompare {
cursor_id: sorter_cursor_id,
sorted_record_reg,
num_regs: columns.len(),
pc_when_nonequal: goto_label,
});
program.emit_insn(Insn::Halt {
err_code: SQLITE_CONSTRAINT_UNIQUE,
description: format_unique_violation_desc(tbl_name.as_str(), &idx),
});
program.preassign_label_to_next_insn(label_after_sorter_compare);
} else {
program.preassign_label_to_next_insn(sorted_loop_start);
}
program.emit_insn(Insn::SorterData { program.emit_insn(Insn::SorterData {
pseudo_cursor: pseudo_cursor_id, pseudo_cursor: pseudo_cursor_id,
cursor_id: sorter_cursor_id, cursor_id: sorter_cursor_id,

View File

@@ -62,7 +62,7 @@ pub struct Sorter {
/// The number of values in the key. /// The number of values in the key.
key_len: usize, key_len: usize,
/// The key info. /// The key info.
index_key_info: Rc<Vec<KeyInfo>>, pub index_key_info: Rc<Vec<KeyInfo>>,
/// Sorted chunks stored on disk. /// Sorted chunks stored on disk.
chunks: Vec<SortedChunk>, chunks: Vec<SortedChunk>,
/// The heap of records consumed from the chunks and their corresponding chunk index. /// The heap of records consumed from the chunks and their corresponding chunk index.

View File

@@ -13,3 +13,49 @@ do_execsql_test_on_specific_db {:memory:} create-index-quoted-identifiers {
"CREATE INDEX \"idx idx\" ON \"t t\" (\"a a\")" "CREATE INDEX \"idx idx\" ON \"t t\" (\"a a\")"
"CREATE UNIQUE INDEX \"unique idx idx\" ON \"t t\" (\"a a\")" "CREATE UNIQUE INDEX \"unique idx idx\" ON \"t t\" (\"a a\")"
} }
# single-column index key: creating unique index fails with duplicates
do_execsql_test_in_memory_any_error create-unique-index-with-duplicates-1 {
CREATE TABLE t1(a, b);
INSERT INTO t1 VALUES(1, 1);
INSERT INTO t1 VALUES(1, 2);
CREATE UNIQUE INDEX idx1 ON t1(a);
}
# multi-column index key: creating unique index fails with duplicates
do_execsql_test_in_memory_any_error create-unique-index-with-duplicates-2 {
CREATE TABLE t2(a, b, c);
INSERT INTO t2 VALUES(1, 2, 3);
INSERT INTO t2 VALUES(1, 2, 4);
CREATE UNIQUE INDEX idx2 ON t2(a, b);
}
# single-column index key: creating unique index succeeds because NULLs are never equal
do_execsql_test_on_specific_db {:memory:} create-unique-index-with-duplicates-3 {
CREATE TABLE t3(a);
INSERT INTO t3 VALUES(NULL);
INSERT INTO t3 VALUES(NULL);
CREATE UNIQUE INDEX idx3 ON t3(a);
SELECT count(*) FROM t3;
} {2}
# multi-column index key: creating unique index succeeds because NULLs are never equal
do_execsql_test_on_specific_db {:memory:} create-unique-index-with-duplicates-4 {
CREATE TABLE t4(a, b);
INSERT INTO t4 VALUES(1, NULL);
INSERT INTO t4 VALUES(1, NULL);
CREATE UNIQUE INDEX idx4 ON t4(a, b);
SELECT count(*) FROM t4;
} {2}
# multi-column index key: creating unique index succeeds when all NULLs
do_execsql_test_on_specific_db {:memory:} create-unique-index-with-duplicates-5 {
CREATE TABLE t5(a, b);
INSERT INTO t5 VALUES(NULL, NULL);
INSERT INTO t5 VALUES(NULL, NULL);
CREATE UNIQUE INDEX idx5 ON t5(a, b);
SELECT count(*) FROM t5;
} {2}