This commit is contained in:
meteorgan 2025-07-07 17:25:09 +07:00 committed by GitHub
commit 38d40ef94d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 248 additions and 48 deletions

View file

@ -6332,7 +6332,11 @@ fn compute_free_space(page: &PageContent, usable_space: u16) -> u16 {
/// Allocate space for a cell on a page.
fn allocate_cell_space(page_ref: &PageContent, amount: u16, usable_space: u16) -> Result<u16> {
let amount = amount as usize;
let mut amount = amount as usize;
// the minimum cell size is 4 bytes, so we need to ensure that we allocate at least that much space.
if amount < 4 {
amount = 4;
}
let (cell_offset, _) = page_ref.cell_pointer_array_offset_and_size();
let gap = cell_offset + 2 * page_ref.cell_count();

View file

@ -668,7 +668,11 @@ impl PageContent {
if overflows {
to_read + n_payload
} else {
len_payload as usize + n_payload
let mut size = len_payload as usize + n_payload;
if size < 4 {
size = 4;
}
size
}
}
PageType::TableLeaf => {
@ -683,7 +687,11 @@ impl PageContent {
if overflows {
to_read + n_payload + n_rowid
} else {
len_payload as usize + n_payload + n_rowid
let mut size = len_payload as usize + n_payload + n_rowid;
if size < 4 {
size = 4;
}
size
}
}
};

View file

@ -150,9 +150,9 @@ fn emit_compound_select(
CompoundOperator::Union => {
let mut new_dedupe_index = false;
let dedupe_index = match right_most.query_destination {
QueryDestination::EphemeralIndex { cursor_id, index } => {
(cursor_id, index.clone())
}
QueryDestination::EphemeralIndex {
cursor_id, index, ..
} => (cursor_id, index.clone()),
_ => {
new_dedupe_index = true;
create_dedupe_index(program, &right_most, schema)?
@ -161,6 +161,7 @@ fn emit_compound_select(
plan.query_destination = QueryDestination::EphemeralIndex {
cursor_id: dedupe_index.0,
index: dedupe_index.1.clone(),
is_delete: false,
};
let compound_select = Plan::CompoundSelect {
left,
@ -182,20 +183,18 @@ fn emit_compound_select(
right_most.query_destination = QueryDestination::EphemeralIndex {
cursor_id: dedupe_index.0,
index: dedupe_index.1.clone(),
is_delete: false,
};
emit_query(program, &mut right_most, &mut right_most_ctx)?;
if new_dedupe_index {
let label_jump_over_dedupe = program.allocate_label();
read_deduplicated_union_rows(
read_deduplicated_union_or_except_rows(
program,
dedupe_index.0,
dedupe_index.1.as_ref(),
limit_ctx,
label_jump_over_dedupe,
yield_reg,
);
program.preassign_label_to_next_insn(label_jump_over_dedupe);
}
}
CompoundOperator::Intersect => {
@ -211,6 +210,7 @@ fn emit_compound_select(
plan.query_destination = QueryDestination::EphemeralIndex {
cursor_id: left_cursor_id,
index: left_index.clone(),
is_delete: false,
};
let compound_select = Plan::CompoundSelect {
left,
@ -234,6 +234,7 @@ fn emit_compound_select(
right_most.query_destination = QueryDestination::EphemeralIndex {
cursor_id: right_cursor_id,
index: right_index,
is_delete: false,
};
emit_query(program, &mut right_most, &mut right_most_ctx)?;
read_intersect_rows(
@ -246,8 +247,49 @@ fn emit_compound_select(
yield_reg,
);
}
_ => {
crate::bail_parse_error!("unimplemented compound select operator: {:?}", operator);
CompoundOperator::Except => {
let mut new_index = false;
let (cursor_id, index) = match right_most.query_destination {
QueryDestination::EphemeralIndex {
cursor_id, index, ..
} => (cursor_id, index),
_ => {
new_index = true;
create_dedupe_index(program, &right_most, schema)?
}
};
plan.query_destination = QueryDestination::EphemeralIndex {
cursor_id,
index: index.clone(),
is_delete: false,
};
let compound_select = Plan::CompoundSelect {
left,
right_most: plan,
limit,
offset,
order_by,
};
emit_compound_select(
program,
compound_select,
schema,
syms,
None,
yield_reg,
reg_result_cols_start,
)?;
right_most.query_destination = QueryDestination::EphemeralIndex {
cursor_id,
index: index.clone(),
is_delete: true,
};
emit_query(program, &mut right_most, &mut right_most_ctx)?;
if new_index {
read_deduplicated_union_or_except_rows(
program, cursor_id, &index, limit_ctx, yield_reg,
);
}
}
},
None => {
@ -302,15 +344,16 @@ fn create_dedupe_index(
Ok((cursor_id, dedupe_index.clone()))
}
/// Emits the bytecode for reading deduplicated rows from the ephemeral index created for UNION operators.
fn read_deduplicated_union_rows(
/// Emits the bytecode for reading deduplicated rows from the ephemeral index created for
/// UNION or EXCEPT operators.
fn read_deduplicated_union_or_except_rows(
program: &mut ProgramBuilder,
dedupe_cursor_id: usize,
dedupe_index: &Index,
limit_ctx: Option<LimitCtx>,
label_limit_reached: BranchOffset,
yield_reg: Option<usize>,
) {
let label_close = program.allocate_label();
let label_dedupe_next = program.allocate_label();
let label_dedupe_loop_start = program.allocate_label();
let dedupe_cols_start_reg = program.alloc_registers(dedupe_index.columns.len());
@ -348,7 +391,7 @@ fn read_deduplicated_union_rows(
if let Some(limit_ctx) = limit_ctx {
program.emit_insn(Insn::DecrJumpZero {
reg: limit_ctx.reg_limit,
target_pc: label_limit_reached,
target_pc: label_close,
})
}
program.preassign_label_to_next_insn(label_dedupe_next);
@ -356,6 +399,7 @@ fn read_deduplicated_union_rows(
cursor_id: dedupe_cursor_id,
pc_if_next: label_dedupe_loop_start,
});
program.preassign_label_to_next_insn(label_close);
program.emit_insn(Insn::Close {
cursor_id: dedupe_cursor_id,
});

View file

@ -562,6 +562,7 @@ fn emit_delete_insns(
start_reg,
num_regs,
cursor_id: index_cursor_id,
raise_error_if_no_matching_entry: true,
});
}
}
@ -1064,6 +1065,7 @@ fn emit_update_insns(
start_reg,
num_regs,
cursor_id: idx_cursor_id,
raise_error_if_no_matching_entry: true,
});
// Insert new index key (filled further above with values from set_clauses)

View file

@ -324,6 +324,8 @@ pub enum QueryDestination {
cursor_id: CursorID,
/// The index that will be used to store the results.
index: Arc<Index>,
/// Whether this is a delete operation that will remove the index entries
is_delete: bool,
},
/// The results of the query are stored in an ephemeral table,
/// later used by the parent query.

View file

@ -85,21 +85,31 @@ pub fn emit_result_row_and_limit(
QueryDestination::EphemeralIndex {
cursor_id: index_cursor_id,
index: dedupe_index,
is_delete,
} => {
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: result_columns_start_reg,
count: plan.result_columns.len(),
dest_reg: record_reg,
index_name: Some(dedupe_index.name.clone()),
});
program.emit_insn(Insn::IdxInsert {
cursor_id: *index_cursor_id,
record_reg,
unpacked_start: None,
unpacked_count: None,
flags: IdxInsertFlags::new(),
});
if *is_delete {
program.emit_insn(Insn::IdxDelete {
start_reg: result_columns_start_reg,
num_regs: plan.result_columns.len(),
cursor_id: *index_cursor_id,
raise_error_if_no_matching_entry: false,
});
} else {
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: result_columns_start_reg,
count: plan.result_columns.len(),
dest_reg: record_reg,
index_name: Some(dedupe_index.name.clone()),
});
program.emit_insn(Insn::IdxInsert {
cursor_id: *index_cursor_id,
record_reg,
unpacked_start: None,
unpacked_count: None,
flags: IdxInsertFlags::new(),
});
}
}
QueryDestination::EphemeralTable {
cursor_id: table_cursor_id,

View file

@ -124,15 +124,6 @@ pub fn prepare_select_plan(
let mut left = Vec::with_capacity(compounds.len());
for CompoundSelect { select, operator } in compounds {
// TODO: add support for EXCEPT
if operator != ast::CompoundOperator::UnionAll
&& operator != ast::CompoundOperator::Union
&& operator != ast::CompoundOperator::Intersect
{
crate::bail_parse_error!(
"only UNION ALL, UNION and INTERSECT are supported for compound SELECTs"
);
}
left.push((last, operator));
last = prepare_one_select_plan(
schema,

View file

@ -4362,6 +4362,7 @@ pub fn op_idx_delete(
cursor_id,
start_reg,
num_regs,
raise_error_if_no_matching_entry,
} = insn
else {
unreachable!("unexpected Insn {:?}", insn)
@ -4377,7 +4378,7 @@ pub fn op_idx_delete(
);
match &state.op_idx_delete_state {
Some(OpIdxDeleteState::Seeking(record)) => {
{
let found = {
let mut cursor = state.get_cursor(*cursor_id);
let cursor = cursor.as_btree_mut();
let found = return_if_io!(
@ -4389,6 +4390,21 @@ pub fn op_idx_delete(
cursor.root_page(),
record
);
found
};
if !found {
// If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching index entry is found
// Also, do not raise this (self-correcting and non-critical) error if in writable_schema mode.
if *raise_error_if_no_matching_entry {
return Err(LimboError::Corrupt(format!(
"IdxDelete: no matching index entry found for record {:?}",
record
)));
}
state.pc += 1;
state.op_idx_delete_state = None;
return Ok(InsnFunctionStepResult::Step);
}
state.op_idx_delete_state = Some(OpIdxDeleteState::Verifying);
}
@ -4399,12 +4415,7 @@ pub fn op_idx_delete(
return_if_io!(cursor.rowid())
};
if rowid.is_none() {
// If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching
// index entry is found. This happens when running an UPDATE or DELETE statement and the
// index entry to be updated or deleted is not found. For some uses of IdxDelete
// (example: the EXCEPT operator) it does not matter that no matching entry is found.
// For those cases, P5 is zero. Also, do not raise this (self-correcting and non-critical) error if in writable_schema mode.
if rowid.is_none() && *raise_error_if_no_matching_entry {
return Err(LimboError::Corrupt(format!(
"IdxDelete: no matching index entry found for record {:?}",
make_record(&state.registers, start_reg, num_regs)

View file

@ -1120,13 +1120,14 @@ pub fn insn_to_str(
cursor_id,
start_reg,
num_regs,
raise_error_if_no_matching_entry,
} => (
"IdxDelete",
*cursor_id as i32,
*start_reg as i32,
*num_regs as i32,
Value::build_text(""),
0,
*raise_error_if_no_matching_entry as u16,
"".to_string(),
),
Insn::NewRowid {

View file

@ -733,10 +733,15 @@ pub enum Insn {
cursor_id: CursorID,
},
/// If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching index entry
/// is found. This happens when running an UPDATE or DELETE statement and the index entry to
/// be updated or deleted is not found. For some uses of IdxDelete (example: the EXCEPT operator)
/// it does not matter that no matching entry is found. For those cases, P5 is zero.
IdxDelete {
start_reg: usize,
num_regs: usize,
cursor_id: CursorID,
raise_error_if_no_matching_entry: bool, // P5
},
NewRowid {

View file

@ -360,6 +360,17 @@ if {[info exists ::env(SQLITE_EXEC)] && ($::env(SQLITE_EXEC) eq "scripts/limbo-s
INSERT INTO t SELECT * FROM t1 INTERSECT SELECT * FROM t2 INTERSECT SELECT * FROM t3;
SELECT * FROM t;
} {2|200}
do_execsql_test_on_specific_db {:memory:} insert_from_select_except {
CREATE TABLE t(a, b);
CREATE TABLE t1(a, b);
CREATE TABLE t2(a, b);
INSERT INTO t1 VALUES (1, 100), (2, 200);
INSERT INTO t2 VALUES (2, 200), (3, 300);
INSERT INTO t SELECT * FROM t1 EXCEPT SELECT * FROM t2;
SELECT * FROM t;
} {1|100}
}
do_execsql_test_on_specific_db {:memory:} negative-primary-integer-key {

View file

@ -449,4 +449,114 @@ if {[info exists ::env(SQLITE_EXEC)] && ($::env(SQLITE_EXEC) eq "scripts/limbo-s
} {a|a
b|b
z|z}
do_execsql_test_on_specific_db {:memory:} select-except-1 {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
select * from t EXCEPT select * from u;
} {y|y}
do_execsql_test_on_specific_db {:memory:} select-except-2 {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('y','y');
select * from t EXCEPT select * from u;
} {}
do_execsql_test_on_specific_db {:memory:} select-except-3 {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('a','y');
INSERT INTO v VALUES('a','x'),('b','y');
select * from t EXCEPT select * from u EXCEPT select * from v;
} {y|y}
do_execsql_test_on_specific_db {:memory:} select-except-limit {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
INSERT INTO t VALUES('a', 'a'),('x','x'),('y','y'),('z','z');
INSERT INTO u VALUES('x','x'),('z','y');
select * from t EXCEPT select * from u limit 2;
} {a|a
y|y}
do_execsql_test_on_specific_db {:memory:} select-except-union-all {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
INSERT INTO v VALUES('x','x'),('y','y');
select * from t EXCEPT select * from u UNION ALL select * from v;
} {y|y
x|x
y|y}
do_execsql_test_on_specific_db {:memory:} select-union-all-except {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
INSERT INTO v VALUES('x','x'),('y','y');
select * from t UNION ALL select * from u EXCEPT select * from v;
} {z|y}
do_execsql_test_on_specific_db {:memory:} select-except-union {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
INSERT INTO v VALUES('x','x'),('z','z');
select * from t EXCEPT select * from u UNION select * from v;
} {x|x
y|y
z|z}
do_execsql_test_on_specific_db {:memory:} select-union-except {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
INSERT INTO v VALUES('x','x'),('z','z');
select * from t UNION select * from u EXCEPT select * from v;
} {y|y
z|y}
do_execsql_test_on_specific_db {:memory:} select-except-intersect {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
INSERT INTO v VALUES('y','y'),('z','z');
select * from t EXCEPT select * from u INTERSECT select * from v;
} {y|y}
do_execsql_test_on_specific_db {:memory:} select-intersect-except {
CREATE TABLE t(x TEXT, y TEXT);
CREATE TABLE u(x TEXT, y TEXT);
CREATE TABLE v(x TEXT, y TEXT);
INSERT INTO t VALUES('x','x'),('y','y');
INSERT INTO u VALUES('x','x'),('z','y');
INSERT INTO v VALUES('x','x'),('z','z');
select * from t INTERSECT select * from u EXCEPT select * from v;
} {}
}

View file

@ -584,7 +584,8 @@ mod tests {
));
}
const COMPOUND_OPERATORS: [&str; 3] = [" UNION ALL ", " UNION ", " INTERSECT "];
const COMPOUND_OPERATORS: [&str; 4] =
[" UNION ALL ", " UNION ", " INTERSECT ", " EXCEPT "];
let mut query = String::new();
for (i, select_statement) in select_statements.iter().enumerate() {