diff --git a/COMPAT.md b/COMPAT.md index 58fbaa4d9..48383a0ac 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -486,7 +486,7 @@ Modifiers: | Lt | Yes | | | MakeRecord | Yes | | | MaxPgcnt | Yes | | -| MemMax | No | | +| MemMax | Yes | | | Move | Yes | | | Multiply | Yes | | | MustBeInt | Yes | | diff --git a/core/error.rs b/core/error.rs index 5eae56b6b..23ce9fe9b 100644 --- a/core/error.rs +++ b/core/error.rs @@ -164,4 +164,5 @@ impl From for LimboError { pub const SQLITE_CONSTRAINT: usize = 19; pub const SQLITE_CONSTRAINT_PRIMARYKEY: usize = SQLITE_CONSTRAINT | (6 << 8); pub const SQLITE_CONSTRAINT_NOTNULL: usize = SQLITE_CONSTRAINT | (5 << 8); +pub const SQLITE_FULL: usize = 13; // we want this in autoincrement - incase if user inserts max allowed int pub const SQLITE_CONSTRAINT_UNIQUE: usize = 2067; diff --git a/core/incremental/compiler.rs b/core/incremental/compiler.rs index 529cc5b23..36d3041f9 100644 --- a/core/incremental/compiler.rs +++ b/core/incremental/compiler.rs @@ -1977,6 +1977,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(users_table)); @@ -2029,6 +2030,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(products_table)); @@ -2092,6 +2094,7 @@ mod tests { }, ], has_rowid: true, + has_autoincrement: false, is_strict: false, unique_sets: vec![], }; @@ -2130,6 +2133,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(customers_table)); @@ -2191,6 +2195,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(purchases_table)); @@ -2240,6 +2245,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(vendors_table)); @@ -2276,6 +2282,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(sales_table)); diff --git a/core/incremental/view.rs b/core/incremental/view.rs index 65fa5e2bb..44692aa8a 100644 --- a/core/incremental/view.rs +++ b/core/incremental/view.rs @@ -1413,6 +1413,7 @@ mod tests { has_rowid: true, is_strict: false, unique_sets: vec![], + has_autoincrement: false, }; // Create orders table @@ -1460,6 +1461,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; @@ -1508,6 +1510,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; @@ -1556,6 +1559,7 @@ mod tests { ], has_rowid: true, // Has implicit rowid but no alias is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; diff --git a/core/lib.rs b/core/lib.rs index 55c9310d2..ae7787d62 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -2498,6 +2498,7 @@ impl Statement { &self.program.sql, )? }; + // Save parameters before they are reset let parameters = std::mem::take(&mut self.state.parameters); let (max_registers, cursor_count) = match self.query_mode { diff --git a/core/schema.rs b/core/schema.rs index 25c459e0d..2f5ebfec6 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -544,6 +544,8 @@ impl Schema { primary_key_columns: Vec::new(), has_rowid: true, is_strict: false, + has_autoincrement: false, + unique_sets: vec![], }))); @@ -882,6 +884,7 @@ pub struct BTreeTable { pub columns: Vec, pub has_rowid: bool, pub is_strict: bool, + pub has_autoincrement: bool, pub unique_sets: Vec, } @@ -1016,6 +1019,7 @@ pub fn create_table( let table_name = normalize_ident(tbl_name); trace!("Creating table {}", table_name); let mut has_rowid = true; + let mut has_autoincrement = false; let mut primary_key_columns = vec![]; let mut cols = vec![]; let is_strict: bool; @@ -1028,7 +1032,16 @@ pub fn create_table( } => { is_strict = options.contains(TableOptions::STRICT); for c in constraints { - if let ast::TableConstraint::PrimaryKey { columns, .. } = &c.constraint { + if let ast::TableConstraint::PrimaryKey { + columns, + auto_increment, + .. + } = &c.constraint + { + if *auto_increment { + has_autoincrement = true; + } + for column in columns { let col_name = match column.expr.as_ref() { Expr::Id(id) => normalize_ident(id.as_str()), @@ -1108,8 +1121,15 @@ pub fn create_table( let mut collation = None; for c_def in constraints { match c_def.constraint { - ast::ColumnConstraint::PrimaryKey { order: o, .. } => { + ast::ColumnConstraint::PrimaryKey { + order: o, + auto_increment, + .. + } => { primary_key = true; + if auto_increment { + has_autoincrement = true; + } if let Some(o) = o { order = o; } @@ -1177,6 +1197,21 @@ pub fn create_table( } } + if has_autoincrement { + // only allow integers + if primary_key_columns.len() != 1 { + crate::bail_parse_error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY"); + } + let pk_col_name = &primary_key_columns[0].0; + let pk_col = cols.iter().find(|c| c.name.as_deref() == Some(pk_col_name)); + + if let Some(col) = pk_col { + if col.ty != Type::Integer { + crate::bail_parse_error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY"); + } + } + } + for col in cols.iter() { if col.is_rowid_alias { // Unique sets are used for creating automatic indexes. An index is not created for a rowid alias PRIMARY KEY. @@ -1197,6 +1232,7 @@ pub fn create_table( name: table_name, has_rowid, primary_key_columns, + has_autoincrement, columns: cols, is_strict, unique_sets: { @@ -1524,6 +1560,7 @@ pub fn sqlite_schema_table() -> BTreeTable { name: "sqlite_schema".to_string(), has_rowid: true, is_strict: false, + has_autoincrement: false, primary_key_columns: vec![], columns: vec![ Column { @@ -2196,6 +2233,7 @@ mod tests { name: "t1".to_string(), has_rowid: true, is_strict: false, + has_autoincrement: false, primary_key_columns: vec![("nonexistent".to_string(), SortOrder::Asc)], columns: vec![Column { name: Some("a".to_string()), diff --git a/core/translate/index.rs b/core/translate/index.rs index 88e90f92b..48d7e27ce 100644 --- a/core/translate/index.rs +++ b/core/translate/index.rs @@ -37,6 +37,9 @@ pub fn translate_create_index( connection: &Arc, where_clause: Option>, ) -> crate::Result { + if tbl_name.eq_ignore_ascii_case("sqlite_sequence") { + crate::bail_parse_error!("table sqlite_sequence may not be indexed"); + } if !schema.indexes_enabled() { crate::bail_parse_error!( "CREATE INDEX is disabled by default. Run with `--experimental-indexes` to enable this feature." diff --git a/core/translate/insert.rs b/core/translate/insert.rs index c13b073cf..6b19ba6df 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -198,6 +198,10 @@ pub fn translate_insert( None }; + if inserting_multiple_rows && btree_table.has_autoincrement { + ensure_sequence_initialized(&mut program, schema, &btree_table)?; + } + let halt_label = program.allocate_label(); let loop_start_label = program.allocate_label(); let row_done_label = program.allocate_label(); @@ -441,37 +445,167 @@ pub fn translate_insert( }); } - // Common record insertion logic for both single and multiple rows let has_user_provided_rowid = insertion.key.is_provided_by_user(); - let check_rowid_is_integer_label = if has_user_provided_rowid { - Some(program.allocate_label()) - } else { - None - }; - if has_user_provided_rowid { - program.emit_insn(Insn::NotNull { - reg: insertion.key_register(), - target_pc: check_rowid_is_integer_label.unwrap(), + let key_ready_for_uniqueness_check_label = program.allocate_label(); + let key_generation_label = program.allocate_label(); + + let mut autoincrement_meta = None; + + if btree_table.has_autoincrement { + let seq_table = schema.get_btree_table("sqlite_sequence").ok_or_else(|| { + crate::error::LimboError::InternalError("sqlite_sequence table not found".to_string()) + })?; + let seq_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(seq_table.clone())); + program.emit_insn(Insn::OpenWrite { + cursor_id: seq_cursor_id, + root_page: seq_table.root_page.into(), + db: 0, }); + + let table_name_reg = program.emit_string8_new_reg(btree_table.name.clone()); + let r_seq = program.alloc_register(); + let r_seq_rowid = program.alloc_register(); + autoincrement_meta = Some((seq_cursor_id, r_seq, r_seq_rowid, table_name_reg)); + + program.emit_insn(Insn::Integer { + dest: r_seq, + value: 0, + }); + program.emit_insn(Insn::Null { + dest: r_seq_rowid, + dest_end: None, + }); + + let loop_start_label = program.allocate_label(); + let loop_end_label = program.allocate_label(); + let found_label = program.allocate_label(); + + program.emit_insn(Insn::Rewind { + cursor_id: seq_cursor_id, + pc_if_empty: loop_end_label, + }); + program.preassign_label_to_next_insn(loop_start_label); + + let name_col_reg = program.alloc_register(); + program.emit_column_or_rowid(seq_cursor_id, 0, name_col_reg); + program.emit_insn(Insn::Ne { + lhs: table_name_reg, + rhs: name_col_reg, + target_pc: found_label, + flags: Default::default(), + collation: None, + }); + + program.emit_column_or_rowid(seq_cursor_id, 1, r_seq); + program.emit_insn(Insn::RowId { + cursor_id: seq_cursor_id, + dest: r_seq_rowid, + }); + program.emit_insn(Insn::Goto { + target_pc: loop_end_label, + }); + + program.preassign_label_to_next_insn(found_label); + program.emit_insn(Insn::Next { + cursor_id: seq_cursor_id, + pc_if_next: loop_start_label, + }); + program.preassign_label_to_next_insn(loop_end_label); } - // Create new rowid if a) not provided by user or b) provided by user but is NULL - program.emit_insn(Insn::NewRowid { - cursor: cursor_id, - rowid_reg: insertion.key_register(), - prev_largest_reg: 0, - }); + if has_user_provided_rowid { + let must_be_int_label = program.allocate_label(); - if let Some(must_be_int_label) = check_rowid_is_integer_label { - program.resolve_label(must_be_int_label, program.offset()); - // If the user provided a rowid, it must be an integer. + program.emit_insn(Insn::NotNull { + reg: insertion.key_register(), + target_pc: must_be_int_label, + }); + + program.emit_insn(Insn::Goto { + target_pc: key_generation_label, + }); + + program.preassign_label_to_next_insn(must_be_int_label); program.emit_insn(Insn::MustBeInt { reg: insertion.key_register(), }); + + program.emit_insn(Insn::Goto { + target_pc: key_ready_for_uniqueness_check_label, + }); } + program.preassign_label_to_next_insn(key_generation_label); + if let Some((_, r_seq, _, _)) = autoincrement_meta { + let r_max = program.alloc_register(); + + let dummy_reg = program.alloc_register(); + + program.emit_insn(Insn::NewRowid { + cursor: cursor_id, + rowid_reg: dummy_reg, + prev_largest_reg: r_max, + }); + + program.emit_insn(Insn::Copy { + src_reg: r_seq, + dst_reg: insertion.key_register(), + extra_amount: 0, + }); + program.emit_insn(Insn::MemMax { + dest_reg: insertion.key_register(), + src_reg: r_max, + }); + + let no_overflow_label = program.allocate_label(); + let max_i64_reg = program.alloc_register(); + program.emit_insn(Insn::Integer { + dest: max_i64_reg, + value: i64::MAX, + }); + program.emit_insn(Insn::Ne { + lhs: insertion.key_register(), + rhs: max_i64_reg, + target_pc: no_overflow_label, + flags: Default::default(), + collation: None, + }); + + program.emit_insn(Insn::Halt { + err_code: crate::error::SQLITE_FULL, + description: "database or disk is full".to_string(), + }); + + program.preassign_label_to_next_insn(no_overflow_label); + + program.emit_insn(Insn::AddImm { + register: insertion.key_register(), + value: 1, + }); + + if let Some((seq_cursor_id, _, r_seq_rowid, table_name_reg)) = autoincrement_meta { + emit_update_sqlite_sequence( + &mut program, + schema, + seq_cursor_id, + r_seq_rowid, + table_name_reg, + insertion.key_register(), + )?; + } + } else { + program.emit_insn(Insn::NewRowid { + cursor: cursor_id, + rowid_reg: insertion.key_register(), + prev_largest_reg: 0, + }); + } + + program.preassign_label_to_next_insn(key_ready_for_uniqueness_check_label); + // Check uniqueness constraint for rowid if it was provided by user. // When the DB allocates it there are no need for separate uniqueness checks. + if has_user_provided_rowid { let make_record_label = program.allocate_label(); program.emit_insn(Insn::NotExists { @@ -864,6 +998,31 @@ pub fn translate_insert( table_name: table_name.to_string(), }); + if let Some((seq_cursor_id, r_seq, r_seq_rowid, table_name_reg)) = autoincrement_meta { + let no_update_needed_label = program.allocate_label(); + program.emit_insn(Insn::Le { + lhs: insertion.key_register(), + rhs: r_seq, + target_pc: no_update_needed_label, + flags: Default::default(), + collation: None, + }); + + emit_update_sqlite_sequence( + &mut program, + schema, + seq_cursor_id, + r_seq_rowid, + table_name_reg, + insertion.key_register(), + )?; + + program.preassign_label_to_next_insn(no_update_needed_label); + program.emit_insn(Insn::Close { + cursor_id: seq_cursor_id, + }); + } + // Emit update in the CDC table if necessary (after the INSERT updated the table) if let Some((cdc_cursor_id, _)) = &cdc_table { let cdc_has_after = program.capture_data_changes_mode().has_after(); @@ -1410,6 +1569,112 @@ fn translate_virtual_table_insert( Ok(program) } +/// makes sure that an AUTOINCREMENT table has a sequence row in `sqlite_sequence`, inserting one with 0 if missing. +fn ensure_sequence_initialized( + program: &mut ProgramBuilder, + schema: &Schema, + table: &schema::BTreeTable, +) -> Result<()> { + let seq_table = schema.get_btree_table("sqlite_sequence").ok_or_else(|| { + crate::error::LimboError::InternalError("sqlite_sequence table not found".to_string()) + })?; + + let seq_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(seq_table.clone())); + + program.emit_insn(Insn::OpenWrite { + cursor_id: seq_cursor_id, + root_page: seq_table.root_page.into(), + db: 0, + }); + + let table_name_reg = program.emit_string8_new_reg(table.name.clone()); + + let loop_start_label = program.allocate_label(); + let entry_exists_label = program.allocate_label(); + let insert_new_label = program.allocate_label(); + + program.emit_insn(Insn::Rewind { + cursor_id: seq_cursor_id, + pc_if_empty: insert_new_label, + }); + + program.preassign_label_to_next_insn(loop_start_label); + + let name_col_reg = program.alloc_register(); + + program.emit_column_or_rowid(seq_cursor_id, 0, name_col_reg); + + program.emit_insn(Insn::Eq { + lhs: table_name_reg, + rhs: name_col_reg, + target_pc: entry_exists_label, + flags: Default::default(), + collation: None, + }); + + program.emit_insn(Insn::Next { + cursor_id: seq_cursor_id, + pc_if_next: loop_start_label, + }); + + program.preassign_label_to_next_insn(insert_new_label); + + let record_reg = program.alloc_register(); + let record_start_reg = program.alloc_registers(2); + let zero_reg = program.alloc_register(); + + program.emit_insn(Insn::Integer { + dest: zero_reg, + value: 0, + }); + + program.emit_insn(Insn::Copy { + src_reg: table_name_reg, + dst_reg: record_start_reg, + extra_amount: 0, + }); + + program.emit_insn(Insn::Copy { + src_reg: zero_reg, + dst_reg: record_start_reg + 1, + extra_amount: 0, + }); + + let affinity_str = seq_table + .columns + .iter() + .map(|c| c.affinity().aff_mask()) + .collect(); + + program.emit_insn(Insn::MakeRecord { + start_reg: record_start_reg, + count: 2, + dest_reg: record_reg, + index_name: None, + affinity_str: Some(affinity_str), + }); + + let new_rowid_reg = program.alloc_register(); + program.emit_insn(Insn::NewRowid { + cursor: seq_cursor_id, + rowid_reg: new_rowid_reg, + prev_largest_reg: 0, + }); + program.emit_insn(Insn::Insert { + cursor: seq_cursor_id, + key_reg: new_rowid_reg, + record_reg, + flag: InsertFlags::new(), + table_name: "sqlite_sequence".to_string(), + }); + + program.preassign_label_to_next_insn(entry_exists_label); + program.emit_insn(Insn::Close { + cursor_id: seq_cursor_id, + }); + + Ok(()) +} #[inline] /// Build the UNIQUE constraint error description to match sqlite /// single column: `t.c1` @@ -1479,3 +1744,75 @@ pub fn rewrite_partial_index_where( }, ) } + +fn emit_update_sqlite_sequence( + program: &mut ProgramBuilder, + schema: &Schema, + seq_cursor_id: usize, + r_seq_rowid: usize, + table_name_reg: usize, + new_key_reg: usize, +) -> Result<()> { + let record_reg = program.alloc_register(); + let record_start_reg = program.alloc_registers(2); + program.emit_insn(Insn::Copy { + src_reg: table_name_reg, + dst_reg: record_start_reg, + extra_amount: 0, + }); + program.emit_insn(Insn::Copy { + src_reg: new_key_reg, + dst_reg: record_start_reg + 1, + extra_amount: 0, + }); + + let seq_table = schema.get_btree_table("sqlite_sequence").unwrap(); + let affinity_str = seq_table + .columns + .iter() + .map(|col| col.affinity().aff_mask()) + .collect::(); + program.emit_insn(Insn::MakeRecord { + start_reg: record_start_reg, + count: 2, + dest_reg: record_reg, + index_name: None, + affinity_str: Some(affinity_str), + }); + + let update_existing_label = program.allocate_label(); + let end_update_label = program.allocate_label(); + program.emit_insn(Insn::NotNull { + reg: r_seq_rowid, + target_pc: update_existing_label, + }); + + program.emit_insn(Insn::NewRowid { + cursor: seq_cursor_id, + rowid_reg: r_seq_rowid, + prev_largest_reg: 0, + }); + program.emit_insn(Insn::Insert { + cursor: seq_cursor_id, + key_reg: r_seq_rowid, + record_reg, + flag: InsertFlags::new(), + table_name: "sqlite_sequence".to_string(), + }); + program.emit_insn(Insn::Goto { + target_pc: end_update_label, + }); + + program.preassign_label_to_next_insn(update_existing_label); + program.emit_insn(Insn::Insert { + cursor: seq_cursor_id, + key_reg: r_seq_rowid, + record_reg, + flag: InsertFlags(turso_parser::ast::ResolveType::Replace.bit_value() as u8), + table_name: "sqlite_sequence".to_string(), + }); + + program.preassign_label_to_next_insn(end_update_label); + + Ok(()) +} diff --git a/core/translate/logical.rs b/core/translate/logical.rs index b11e2df4f..4c27a506b 100644 --- a/core/translate/logical.rs +++ b/core/translate/logical.rs @@ -2333,6 +2333,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(users_table)); @@ -2394,6 +2395,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(orders_table)); @@ -2455,6 +2457,7 @@ mod tests { ], has_rowid: true, is_strict: false, + has_autoincrement: false, unique_sets: vec![], }; schema.add_btree_table(Arc::new(products_table)); diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 1a963b5b3..2263e3a97 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -182,12 +182,12 @@ pub fn translate_inner( } => translate_create_table( tbl_name, temporary, - body, if_not_exists, + body, schema, syms, - connection, program, + connection, )?, ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"), ast::Stmt::CreateView { diff --git a/core/translate/optimizer/join.rs b/core/translate/optimizer/join.rs index 617bc94dd..db5e71000 100644 --- a/core/translate/optimizer/join.rs +++ b/core/translate/optimizer/join.rs @@ -1658,6 +1658,7 @@ mod tests { Arc::new(BTreeTable { root_page: 1, // Page number doesn't matter for tests name: name.to_string(), + has_autoincrement: false, primary_key_columns: vec![], columns, has_rowid: true, diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 99b66f215..1eb5d9cf5 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -453,6 +453,8 @@ fn parse_table( primary_key_columns: Vec::new(), has_rowid: true, is_strict: false, + has_autoincrement: false, + unique_sets: vec![], }); drop(view_guard); diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index a50880611..784176e17 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -287,25 +287,26 @@ fn update_pragma( // but for now, let's keep it as is... let opts = CaptureDataChangesMode::parse(&value)?; if let Some(table) = &opts.table() { - // make sure that we have table created - program = translate_create_table( - QualifiedName { - db_name: None, - name: ast::Name::new(table), - alias: None, - }, - false, - ast::CreateTableBody::ColumnsAndConstraints { - columns: turso_cdc_table_columns(), - constraints: vec![], - options: ast::TableOptions::NONE, - }, - true, - schema, - syms, - &connection, - program, - )?; + if schema.get_table(table).is_none() { + program = translate_create_table( + QualifiedName { + db_name: None, + name: ast::Name::new(table), + alias: None, + }, + false, + true, // if_not_exists + ast::CreateTableBody::ColumnsAndConstraints { + columns: turso_cdc_table_columns(), + constraints: vec![], + options: ast::TableOptions::NONE, + }, + schema, + syms, + program, + &connection, + )?; + } } connection.set_capture_data_changes(opts); Ok((program, TransactionMode::Write)) diff --git a/core/translate/schema.rs b/core/translate/schema.rs index d233ec427..d961eb8ce 100644 --- a/core/translate/schema.rs +++ b/core/translate/schema.rs @@ -21,6 +21,7 @@ use crate::util::PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX; use crate::vdbe::builder::CursorType; use crate::vdbe::insn::Cookie; use crate::vdbe::insn::{CmpInsFlags, InsertFlags, Insn}; +use crate::Connection; use crate::SymbolTable; use crate::{bail_parse_error, Result}; @@ -30,13 +31,14 @@ use turso_ext::VTabKind; pub fn translate_create_table( tbl_name: ast::QualifiedName, temporary: bool, - body: ast::CreateTableBody, if_not_exists: bool, + body: ast::CreateTableBody, schema: &Schema, syms: &SymbolTable, - connection: &Arc, mut program: ProgramBuilder, + connection: &Connection, ) -> Result { + let normalized_tbl_name = normalize_ident(tbl_name.name.as_str()); if temporary { bail_parse_error!("TEMPORARY table not supported yet"); } @@ -56,7 +58,6 @@ pub fn translate_create_table( approx_num_labels: 1, }; program.extend(&opts); - let normalized_tbl_name = normalize_ident(tbl_name.name.as_str()); if schema.get_table(&normalized_tbl_name).is_some() { if if_not_exists { return Ok(program); @@ -64,6 +65,79 @@ pub fn translate_create_table( bail_parse_error!("Table {} already exists", normalized_tbl_name); } + let mut has_autoincrement = false; + if let ast::CreateTableBody::ColumnsAndConstraints { + columns, + constraints, + .. + } = &body + { + for col in columns { + for constraint in &col.constraints { + if let ast::ColumnConstraint::PrimaryKey { auto_increment, .. } = + constraint.constraint + { + if auto_increment { + has_autoincrement = true; + break; + } + } + } + if has_autoincrement { + break; + } + } + if !has_autoincrement { + for constraint in constraints { + if let ast::TableConstraint::PrimaryKey { auto_increment, .. } = + constraint.constraint + { + if auto_increment { + has_autoincrement = true; + break; + } + } + } + } + } + + let schema_master_table = schema.get_btree_table(SQLITE_TABLEID).unwrap(); + let sqlite_schema_cursor_id = + program.alloc_cursor_id(CursorType::BTreeTable(schema_master_table.clone())); + program.emit_insn(Insn::OpenWrite { + cursor_id: sqlite_schema_cursor_id, + root_page: 1usize.into(), + db: 0, + }); + let resolver = Resolver::new(schema, syms); + let cdc_table = prepare_cdc_if_necessary(&mut program, schema, SQLITE_TABLEID)?; + + let created_sequence_table = + if has_autoincrement && schema.get_table("sqlite_sequence").is_none() { + let seq_table_root_reg = program.alloc_register(); + program.emit_insn(Insn::CreateBtree { + db: 0, + root: seq_table_root_reg, + flags: CreateBTreeFlags::new_table(), + }); + + let seq_sql = "CREATE TABLE sqlite_sequence(name,seq)"; + emit_schema_entry( + &mut program, + &resolver, + sqlite_schema_cursor_id, + cdc_table.as_ref().map(|x| x.0), + SchemaEntryType::Table, + "sqlite_sequence", + "sqlite_sequence", + seq_table_root_reg, + Some(seq_sql.to_string()), + )?; + true + } else { + false + }; + let sql = create_table_body_to_str(&tbl_name, &body); let parse_schema_label = program.allocate_label(); @@ -72,7 +146,6 @@ pub fn translate_create_table( // TODO: SetCookie // TODO: SetCookie - // Create the table B-tree let table_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: 0, @@ -128,12 +201,12 @@ pub fn translate_create_table( let cdc_table = prepare_cdc_if_necessary(&mut program, schema, SQLITE_TABLEID)?; let resolver = Resolver::new(schema, syms); - // Add the table entry to sqlite_schema + emit_schema_entry( &mut program, &resolver, sqlite_schema_cursor_id, - cdc_table.map(|x| x.0), + cdc_table.as_ref().map(|x| x.0), SchemaEntryType::Table, &normalized_tbl_name, &normalized_tbl_name, @@ -141,7 +214,6 @@ pub fn translate_create_table( Some(sql), )?; - // If we need an automatic index, add its entry to sqlite_schema if let Some(index_regs) = index_regs { for (idx, index_reg) in index_regs.into_iter().enumerate() { let index_name = format!( @@ -171,9 +243,14 @@ pub fn translate_create_table( value: schema.schema_version as i32 + 1, p5: 0, }); + // TODO: remove format, it sucks for performance but is convenient - let parse_schema_where_clause = + let mut parse_schema_where_clause = format!("tbl_name = '{normalized_tbl_name}' AND type != 'trigger'"); + if created_sequence_table { + parse_schema_where_clause.push_str(" OR tbl_name = 'sqlite_sequence'"); + } + program.emit_insn(Insn::ParseSchema { db: sqlite_schema_cursor_id, where_clause: Some(parse_schema_where_clause), @@ -225,16 +302,16 @@ pub fn emit_schema_entry( program.emit_string8_new_reg(name.to_string()); program.emit_string8_new_reg(tbl_name.to_string()); - let rootpage_reg = program.alloc_register(); + let table_root_reg = program.alloc_register(); if root_page_reg == 0 { program.emit_insn(Insn::Integer { - dest: rootpage_reg, + dest: table_root_reg, value: 0, // virtual tables in sqlite always have rootpage=0 }); } else { program.emit_insn(Insn::Copy { src_reg: root_page_reg, - dst_reg: rootpage_reg, + dst_reg: table_root_reg, extra_amount: 0, }); } @@ -496,13 +573,22 @@ pub fn translate_drop_table( syms: &SymbolTable, mut program: ProgramBuilder, ) -> Result { + if tbl_name + .name + .as_str() + .eq_ignore_ascii_case("sqlite_sequence") + { + bail_parse_error!("table sqlite_sequence may not be dropped"); + } + if !schema.indexes_enabled() && schema.table_has_indexes(&tbl_name.name.to_string()) { bail_parse_error!( "DROP TABLE with indexes on the table is disabled by default. Omit the `--experimental-indexes=false` flag to enable this feature." ); } + let opts = ProgramBuilderOpts { - num_cursors: 3, + num_cursors: 4, approx_num_insns: 40, approx_num_labels: 4, }; @@ -556,7 +642,7 @@ pub fn translate_drop_table( }); program.preassign_label_to_next_insn(metadata_loop); - // start loop on schema table + // start loop on schema table program.emit_column_or_rowid( sqlite_schema_cursor_id_0, 2, @@ -587,7 +673,7 @@ pub fn translate_drop_table( dest: row_id_reg, }); if let Some((cdc_cursor_id, _)) = cdc_table { - let table_type = program.emit_string8_new_reg("table".to_string()); // r4 + let table_type = program.emit_string8_new_reg("table".to_string()); // r4 program.mark_last_insn_constant(); let skip_cdc_label = program.allocate_label(); @@ -636,7 +722,7 @@ pub fn translate_drop_table( pc_if_next: metadata_loop, }); program.preassign_label_to_next_insn(end_metadata_label); - // end of loop on schema table + // end of loop on schema table // 2. Destroy the indices within a loop let indices = schema.get_indices(tbl_name.name.as_str()); @@ -685,17 +771,18 @@ pub fn translate_drop_table( let schema_row_id_register = program.alloc_register(); program.emit_null(schema_data_register, Some(schema_row_id_register)); - // All of the following processing needs to be done only if the table is not a virtual table + // All of the following processing needs to be done only if the table is not a virtual table if table.btree().is_some() { - // 4. Open an ephemeral table, and read over the entry from the schema table whose root page was moved in the destroy operation + // 4. Open an ephemeral table, and read over the entry from the schema table whose root page was moved in the destroy operation - // cursor id 1 + // cursor id 1 let sqlite_schema_cursor_id_1 = program.alloc_cursor_id(CursorType::BTreeTable(schema_table.clone())); let simple_table_rc = Arc::new(BTreeTable { root_page: 0, // Not relevant for ephemeral table definition name: "ephemeral_scratch".to_string(), has_rowid: true, + has_autoincrement: false, primary_key_columns: vec![], columns: vec![Column { name: Some("rowid".to_string()), @@ -712,7 +799,7 @@ pub fn translate_drop_table( is_strict: false, unique_sets: vec![], }); - // cursor id 2 + // cursor id 2 let ephemeral_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(simple_table_rc)); program.emit_insn(Insn::OpenEphemeral { cursor_id: ephemeral_cursor_id, @@ -747,9 +834,9 @@ pub fn translate_drop_table( pc_if_empty: copy_schema_to_temp_table_loop_end_label, }); program.preassign_label_to_next_insn(copy_schema_to_temp_table_loop); - // start loop on schema table + // start loop on schema table program.emit_column_or_rowid(sqlite_schema_cursor_id_1, 3, prev_root_page_register); - // The label and Insn::Ne are used to skip over any rows in the schema table that don't have the root page that was moved + // The label and Insn::Ne are used to skip over any rows in the schema table that don't have the root page that was moved let next_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: prev_root_page_register, @@ -776,18 +863,18 @@ pub fn translate_drop_table( pc_if_next: copy_schema_to_temp_table_loop, }); program.preassign_label_to_next_insn(copy_schema_to_temp_table_loop_end_label); - // End loop to copy over row id's from the schema table for rows that have the same root page as the one that was moved + // End loop to copy over row id's from the schema table for rows that have the same root page as the one that was moved program.resolve_label(if_not_label, program.offset()); - // 5. Open a write cursor to the schema table and re-insert the records placed in the ephemeral table but insert the correct root page now + // 5. Open a write cursor to the schema table and re-insert the records placed in the ephemeral table but insert the correct root page now program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id_1, root_page: 1usize.into(), db: 0, }); - // Loop to copy over row id's from the ephemeral table and then re-insert into the schema table with the correct root page + // Loop to copy over row id's from the ephemeral table and then re-insert into the schema table with the correct root page let copy_temp_table_to_schema_loop_end_label = program.allocate_label(); let copy_temp_table_to_schema_loop = program.allocate_label(); program.emit_insn(Insn::Rewind { @@ -844,10 +931,59 @@ pub fn translate_drop_table( pc_if_next: copy_temp_table_to_schema_loop, }); program.preassign_label_to_next_insn(copy_temp_table_to_schema_loop_end_label); - // End loop to copy over row id's from the ephemeral table and then re-insert into the schema table with the correct root page + // End loop to copy over row id's from the ephemeral table and then re-insert into the schema table with the correct root page } - // Drop the in-memory structures for the table + // if drops table, sequence table should reset. + if let Some(seq_table) = schema.get_table("sqlite_sequence").and_then(|t| t.btree()) { + let seq_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(seq_table.clone())); + let seq_table_name_reg = program.alloc_register(); + let dropped_table_name_reg = + program.emit_string8_new_reg(tbl_name.name.as_str().to_string()); + program.mark_last_insn_constant(); + + program.emit_insn(Insn::OpenWrite { + cursor_id: seq_cursor_id, + root_page: seq_table.root_page.into(), + db: 0, + }); + + let end_loop_label = program.allocate_label(); + let loop_start_label = program.allocate_label(); + + program.emit_insn(Insn::Rewind { + cursor_id: seq_cursor_id, + pc_if_empty: end_loop_label, + }); + + program.preassign_label_to_next_insn(loop_start_label); + + program.emit_column_or_rowid(seq_cursor_id, 0, seq_table_name_reg); + + let continue_loop_label = program.allocate_label(); + program.emit_insn(Insn::Ne { + lhs: seq_table_name_reg, + rhs: dropped_table_name_reg, + target_pc: continue_loop_label, + flags: CmpInsFlags::default(), + collation: None, + }); + + program.emit_insn(Insn::Delete { + cursor_id: seq_cursor_id, + table_name: "sqlite_sequence".to_string(), + }); + + program.resolve_label(continue_loop_label, program.offset()); + program.emit_insn(Insn::Next { + cursor_id: seq_cursor_id, + pc_if_next: loop_start_label, + }); + + program.preassign_label_to_next_insn(end_loop_label); + } + + // Drop the in-memory structures for the table program.emit_insn(Insn::DropTable { db: 0, _p2: 0, diff --git a/core/translate/update.rs b/core/translate/update.rs index 278ae30de..447b3c8ee 100644 --- a/core/translate/update.rs +++ b/core/translate/update.rs @@ -298,6 +298,7 @@ pub fn prepare_update_plan( root_page: 0, // Not relevant for ephemeral table definition name: "ephemeral_scratch".to_string(), has_rowid: true, + has_autoincrement: false, primary_key_columns: vec![], columns: vec![Column { name: Some("rowid".to_string()), diff --git a/core/translate/view.rs b/core/translate/view.rs index 9ff8e6c89..bc0c4ff33 100644 --- a/core/translate/view.rs +++ b/core/translate/view.rs @@ -76,6 +76,8 @@ pub fn translate_create_materialized_view( primary_key_columns: vec![], // Materialized views use implicit rowid has_rowid: true, is_strict: false, + has_autoincrement: false, + unique_sets: vec![], }); diff --git a/core/translate/window.rs b/core/translate/window.rs index f4b9fa7d3..3fb512303 100644 --- a/core/translate/window.rs +++ b/core/translate/window.rs @@ -513,6 +513,7 @@ pub fn init_window<'a>( columns: src_columns, is_strict: false, unique_sets: vec![], + has_autoincrement: false, }); let cursor_buffer_read = program.alloc_cursor_id(CursorType::BTreeTable(buffer_table.clone())); let cursor_buffer_write = program.alloc_cursor_id(CursorType::BTreeTable(buffer_table.clone())); diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 282773566..5b5d1232d 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -1990,6 +1990,29 @@ pub fn op_make_record( Ok(InsnFunctionStepResult::Step) } +pub fn op_mem_max( + program: &Program, + state: &mut ProgramState, + insn: &Insn, + pager: &Arc, + mv_store: Option<&Arc>, +) -> Result { + load_insn!(MemMax { dest_reg, src_reg }, insn); + + let dest_val = state.registers[*dest_reg].get_value(); + let src_val = state.registers[*src_reg].get_value(); + + let dest_int = extract_int_value(dest_val); + let src_int = extract_int_value(src_val); + + if dest_int < src_int { + state.registers[*dest_reg] = Register::Value(Value::Integer(src_int)); + } + + state.pc += 1; + Ok(InsnFunctionStepResult::Step) +} + pub fn op_result_row( program: &Program, state: &mut ProgramState, @@ -5679,7 +5702,7 @@ pub fn op_insert( let cursor = cursor.as_btree_mut(); cursor.root_page() }; - if root_page != 1 { + if root_page != 1 && table_name != "sqlite_sequence" { state.op_insert_state.sub_state = OpInsertSubState::UpdateLastRowid; } else { let schema = program.connection.schema.read(); @@ -6937,6 +6960,7 @@ pub fn op_parse_schema( conn.auto_commit .store(previous_auto_commit, Ordering::SeqCst); maybe_nested_stmt_err?; + state.pc += 1; Ok(InsnFunctionStepResult::Step) } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index c5d35f2a3..ce9bd4f4f 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -1759,6 +1759,15 @@ pub fn insn_to_row( 0, String::new(), ), + Insn::MemMax { dest_reg, src_reg } => ( + "MemMax", + *dest_reg as i32, + *src_reg as i32, + 0, + Value::build_text(""), + 0, + format!("r[{dest_reg}]=Max(r[{dest_reg}],r[{src_reg}])"), + ), Insn::Sequence{ cursor_id, target_reg} => ( "Sequence", *cursor_id as i32, diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index cbcf28372..85cd9e545 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -196,6 +196,16 @@ pub enum Insn { rhs: usize, dest: usize, }, + /// Updates the value of register dest_reg to the maximum of its current + /// value and the value in src_reg. + /// + /// - dest_reg = max(int(dest_reg), int(src_reg)) + /// + /// Both registers are converted to integers before the comparison. + MemMax { + dest_reg: usize, // P1 + src_reg: usize, // P2 + }, /// Divide lhs by rhs and store the result in a third register. Divide { lhs: usize, @@ -1272,7 +1282,6 @@ impl InsnVariants { InsnVariants::CreateBtree => execute::op_create_btree, InsnVariants::Destroy => execute::op_destroy, InsnVariants::ResetSorter => execute::op_reset_sorter, - InsnVariants::DropTable => execute::op_drop_table, InsnVariants::DropView => execute::op_drop_view, InsnVariants::Close => execute::op_close, @@ -1309,6 +1318,7 @@ impl InsnVariants { InsnVariants::IfNeg => execute::op_if_neg, InsnVariants::Explain => execute::op_noop, InsnVariants::OpenDup => execute::op_open_dup, + InsnVariants::MemMax => execute::op_mem_max, InsnVariants::Sequence => execute::op_sequence, InsnVariants::SequenceTest => execute::op_sequence_test, } diff --git a/sync/engine/src/database_tape.rs b/sync/engine/src/database_tape.rs index b8dfdb820..bce8acc79 100644 --- a/sync/engine/src/database_tape.rs +++ b/sync/engine/src/database_tape.rs @@ -1088,11 +1088,24 @@ mod tests { assert_eq!( rows, vec![ + vec![ + turso_core::Value::Text(turso_core::types::Text::new("table")), + turso_core::Value::Text(turso_core::types::Text::new( + "sqlite_sequence" + )), + turso_core::Value::Text(turso_core::types::Text::new( + "sqlite_sequence" + )), + turso_core::Value::Integer(2), + turso_core::Value::Text(turso_core::types::Text::new( + "CREATE TABLE sqlite_sequence(name,seq)" + )), + ], vec![ turso_core::Value::Text(turso_core::types::Text::new("table")), turso_core::Value::Text(turso_core::types::Text::new("t")), turso_core::Value::Text(turso_core::types::Text::new("t")), - turso_core::Value::Integer(3), + turso_core::Value::Integer(4), turso_core::Value::Text(turso_core::types::Text::new( "CREATE TABLE t (x TEXT PRIMARY KEY, y)" )), @@ -1101,7 +1114,7 @@ mod tests { turso_core::Value::Text(turso_core::types::Text::new("table")), turso_core::Value::Text(turso_core::types::Text::new("q")), turso_core::Value::Text(turso_core::types::Text::new("q")), - turso_core::Value::Integer(5), + turso_core::Value::Integer(6), turso_core::Value::Text(turso_core::types::Text::new( "CREATE TABLE q (x TEXT PRIMARY KEY, y)" )), @@ -1176,7 +1189,7 @@ mod tests { turso_core::Value::Text(turso_core::types::Text::new("table")), turso_core::Value::Text(turso_core::types::Text::new("t")), turso_core::Value::Text(turso_core::types::Text::new("t")), - turso_core::Value::Integer(3), + turso_core::Value::Integer(4), turso_core::Value::Text(turso_core::types::Text::new( "CREATE TABLE t (x TEXT PRIMARY KEY, y)" )), @@ -1185,7 +1198,7 @@ mod tests { turso_core::Value::Text(turso_core::types::Text::new("index")), turso_core::Value::Text(turso_core::types::Text::new("t_idx")), turso_core::Value::Text(turso_core::types::Text::new("t")), - turso_core::Value::Integer(5), + turso_core::Value::Integer(6), turso_core::Value::Text(turso_core::types::Text::new( "CREATE INDEX t_idx ON t (y)" )), @@ -1260,7 +1273,7 @@ mod tests { turso_core::Value::Text(turso_core::types::Text::new("table")), turso_core::Value::Text(turso_core::types::Text::new("t")), turso_core::Value::Text(turso_core::types::Text::new("t")), - turso_core::Value::Integer(3), + turso_core::Value::Integer(4), turso_core::Value::Text(turso_core::types::Text::new( "CREATE TABLE t (x TEXT PRIMARY KEY, z)" )), diff --git a/testing/all.test b/testing/all.test index aa89838d7..1651dc0ae 100755 --- a/testing/all.test +++ b/testing/all.test @@ -2,6 +2,8 @@ set testdir [file dirname $argv0] +source $testdir/autoincr.test + source $testdir/cmdlineshell.test source $testdir/alter_table.test source $testdir/agg-functions.test diff --git a/testing/autoincr.test b/testing/autoincr.test new file mode 100755 index 000000000..cb28e8c5d --- /dev/null +++ b/testing/autoincr.test @@ -0,0 +1,177 @@ +#!/usr/bin/env tclsh + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + + +# Test: Create an AUTOINCREMENT table and verify sqlite_sequence is also created. +do_execsql_test_on_specific_db {:memory:} autoinc-create-sequence-table { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + SELECT name FROM sqlite_master WHERE type='table' ORDER BY name; +} {sqlite_sequence t1} + +# Test: The sqlite_sequence table is initially empty after table creation. +do_execsql_test_on_specific_db {:memory:} autoinc-sequence-table-is-initially-empty { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + SELECT * FROM sqlite_sequence; +} {} + +# Test: You cannot drop the sqlite_sequence table. +do_execsql_test_in_memory_any_error autoinc-fail-drop-sequence-table { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + DROP TABLE sqlite_sequence; +} + +# Test: You cannot create an index on the sqlite_sequence table. +do_execsql_test_in_memory_any_error autoinc-fail-index-sequence-table { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + CREATE INDEX seqidx ON sqlite_sequence(name); +} + +# Test: First insert populates sqlite_sequence. +do_execsql_test_on_specific_db {:memory:} autoinc-first-insert-populates-sequence { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(12,34); + SELECT * FROM sqlite_sequence; +} {t1|12} + +# Test: Inserting a value larger than the current sequence updates the sequence. +do_execsql_test_on_specific_db {:memory:} autoinc-larger-insert-updates-sequence { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(12,34); + INSERT INTO t1 VALUES(123,456); + SELECT * FROM sqlite_sequence; +} {t1|123} + +# Test: Inserting NULL generates a key one greater than the max. +do_execsql_test_on_specific_db {:memory:} autoinc-null-insert-increments-sequence { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(123,456); + INSERT INTO t1 VALUES(NULL,567); + SELECT * FROM sqlite_sequence; +} {t1|124} + +# Test: Sequence value is not decreased after a DELETE. +do_execsql_test_on_specific_db {:memory:} autoinc-sequence-not-decremented-by-delete { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(123,456); + INSERT INTO t1 VALUES(NULL,567); + DELETE FROM t1 WHERE y=567; + SELECT * FROM sqlite_sequence; +} {t1|124} + +# Test: A NULL insert after a DELETE continues from the previous high-water mark. +do_execsql_test_on_specific_db {:memory:} autoinc-insert-after-delete-uses-high-water-mark { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(125, 1); + DELETE FROM t1; + INSERT INTO t1 VALUES(NULL, 2); + SELECT x FROM t1; +} {126} + +# Test: Clearing a table does not reset the sequence. +do_execsql_test_on_specific_db {:memory:} autoinc-delete-all-does-not-reset-sequence { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(125, 456); + DELETE FROM t1; + SELECT * FROM sqlite_sequence; +} {t1|125} + +# Test: Manually updating the sequence table affects the next generated key. +do_execsql_test_on_specific_db {:memory:} autoinc-manual-update-to-sequence-table { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t1 VALUES(1, 1); + UPDATE sqlite_sequence SET seq=1234 WHERE name='t1'; + INSERT INTO t1 VALUES(NULL, 2); + SELECT * FROM t1 ORDER BY x; +} {1|1 1235|2} + +# Test: AUTOINCREMENT works for multiple tables independently. +do_execsql_test_on_specific_db {:memory:} autoinc-multiple-tables { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + CREATE TABLE t2(d, e INTEGER PRIMARY KEY AUTOINCREMENT, f); + CREATE TABLE t3(g INTEGER PRIMARY KEY AUTOINCREMENT, h); + INSERT INTO t1(y) VALUES('a'); + INSERT INTO t2(d) VALUES('b'); + INSERT INTO t2(d) VALUES('c'); + INSERT INTO t3(h) VALUES('d'); + INSERT INTO t1(x) VALUES(100); + SELECT name, seq FROM sqlite_sequence ORDER BY name; +} {t1|100 t2|2 t3|1} + +# Test: Dropping an AUTOINCREMENT table removes its entry from sqlite_sequence. +do_execsql_test_on_specific_db {:memory:} autoinc-drop-table-removes-sequence-entry { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + CREATE TABLE t2(d, e INTEGER PRIMARY KEY AUTOINCREMENT, f); + INSERT INTO t1(y) VALUES('a'); + INSERT INTO t2(d) VALUES('b'); + DROP TABLE t1; + SELECT name FROM sqlite_sequence; +} {t2} + +# Test: When the last AUTOINCREMENT table is dropped, the sequence table remains but is empty. +do_execsql_test_on_specific_db {:memory:} autoinc-drop-last-table-empties-sequence { + CREATE TABLE t1(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + DROP TABLE t1; + SELECT * FROM sqlite_sequence; +} {} + +# Test: AUTOINCREMENT fails if the maximum rowid is reached. (Assumes 64-bit rowid) +do_execsql_test_in_memory_any_error autoinc-fail-on-max-rowid { + CREATE TABLE t6(v INTEGER PRIMARY KEY AUTOINCREMENT, w); + INSERT INTO t6 VALUES(9223372036854775807, 1); + INSERT INTO t6 VALUES(NULL, 2); +} + +# Test: AUTOINCREMENT keyword is allowed in a separate PRIMARY KEY clause. +do_execsql_test_on_specific_db {:memory:} autoinc-keyword-in-pk-clause { + CREATE TABLE t7(x INTEGER, y REAL, PRIMARY KEY(x AUTOINCREMENT)); + INSERT INTO t7(y) VALUES(123); + INSERT INTO t7(y) VALUES(234); + DELETE FROM t7; + INSERT INTO t7(y) VALUES(345); + SELECT * FROM t7; +} {3|345.0} + +# Test: AUTOINCREMENT fails if the primary key is not an INTEGER. +do_execsql_test_in_memory_any_error autoinc-fail-on-non-integer-pk { + CREATE TABLE t8(x TEXT PRIMARY KEY AUTOINCREMENT); +} + +# Test: An empty INSERT...SELECT does not damage the sequence table. (Ticket #3148) +do_execsql_test_on_specific_db {:memory:} autoinc-empty-insert-select-is-safe { + CREATE TABLE t2(x INTEGER PRIMARY KEY AUTOINCREMENT, y); + INSERT INTO t2 VALUES(NULL, 1); + CREATE TABLE t3(a INTEGER PRIMARY KEY AUTOINCREMENT, b); + INSERT INTO t3 SELECT * FROM t2 WHERE y > 1; + SELECT * FROM sqlite_sequence WHERE name='t3'; +} {t3|0} + +# Test: AUTOINCREMENT with the xfer optimization. (Ticket 7b3328086a5c1) +do_execsql_test_on_specific_db {:memory:} autoinc-with-xfer-optimization { + CREATE TABLE t10a(a INTEGER PRIMARY KEY AUTOINCREMENT, b UNIQUE); + INSERT INTO t10a VALUES(888,9999); + CREATE TABLE t10b(x INTEGER PRIMARY KEY AUTOINCREMENT, y UNIQUE); + INSERT INTO t10b SELECT * FROM t10a; + SELECT * FROM sqlite_sequence ORDER BY name; +} {t10a|888 t10b|888} + +# Test: AUTOINCREMENT works correctly with UPSERT. +do_execsql_test_on_specific_db {:memory:} autoinc-with-upsert { + CREATE TABLE t11(a INTEGER PRIMARY KEY AUTOINCREMENT, b UNIQUE); + INSERT INTO t11(a,b) VALUES(2,3),(5,6),(4,3),(1,2) + ON CONFLICT(b) DO UPDATE SET a=a+1000; + SELECT seq FROM sqlite_sequence WHERE name='t11'; +} {5} + + +# refer https://github.com/tursodatabase/turso/pull/2983#issuecomment-3322404270 was discovered while adding autoincr to fuzz tests +do_execsql_test_on_specific_db {:memory:} autoinc-conflict-on-nothing { + CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, k TEXT); + CREATE UNIQUE INDEX idx_k_partial ON t(k) WHERE id > 1; + INSERT INTO t (k) VALUES ('a'); + INSERT INTO t (k) VALUES ('a'); + INSERT INTO t (k) VALUES ('a') ON CONFLICT DO NOTHING; + INSERT INTO t (k) VALUES ('b'); + SELECT * FROM t ORDER BY id; +} {1|a 2|a 4|b} \ No newline at end of file diff --git a/tests/integration/functions/test_cdc.rs b/tests/integration/functions/test_cdc.rs index cec81d11c..d631c1f93 100644 --- a/tests/integration/functions/test_cdc.rs +++ b/tests/integration/functions/test_cdc.rs @@ -955,13 +955,13 @@ fn test_cdc_schema_changes() { Value::Null, Value::Integer(1), Value::Text("sqlite_schema".to_string()), - Value::Integer(2), + Value::Integer(3), Value::Null, Value::Blob(record([ Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x, y, z UNIQUE, q, PRIMARY KEY (x, y))".to_string() ) @@ -973,13 +973,13 @@ fn test_cdc_schema_changes() { Value::Null, Value::Integer(1), Value::Text("sqlite_schema".to_string()), - Value::Integer(5), + Value::Integer(6), Value::Null, Value::Blob(record([ Value::Text("table".to_string()), Value::Text("q".to_string()), Value::Text("q".to_string()), - Value::Integer(6), + Value::Integer(7), Value::Text("CREATE TABLE q (a, b, c)".to_string()) ])), Value::Null, @@ -989,13 +989,13 @@ fn test_cdc_schema_changes() { Value::Null, Value::Integer(1), Value::Text("sqlite_schema".to_string()), - Value::Integer(6), + Value::Integer(7), Value::Null, Value::Blob(record([ Value::Text("index".to_string()), Value::Text("t_q".to_string()), Value::Text("t".to_string()), - Value::Integer(7), + Value::Integer(8), Value::Text("CREATE INDEX t_q ON t (q)".to_string()) ])), Value::Null, @@ -1005,13 +1005,13 @@ fn test_cdc_schema_changes() { Value::Null, Value::Integer(1), Value::Text("sqlite_schema".to_string()), - Value::Integer(7), + Value::Integer(8), Value::Null, Value::Blob(record([ Value::Text("index".to_string()), Value::Text("q_abc".to_string()), Value::Text("q".to_string()), - Value::Integer(8), + Value::Integer(9), Value::Text("CREATE INDEX q_abc ON q (a, b, c)".to_string()) ])), Value::Null, @@ -1021,12 +1021,12 @@ fn test_cdc_schema_changes() { Value::Null, Value::Integer(-1), Value::Text("sqlite_schema".to_string()), - Value::Integer(2), + Value::Integer(3), Value::Blob(record([ Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x, y, z UNIQUE, q, PRIMARY KEY (x, y))".to_string() ) @@ -1039,12 +1039,12 @@ fn test_cdc_schema_changes() { Value::Null, Value::Integer(-1), Value::Text("sqlite_schema".to_string()), - Value::Integer(7), + Value::Integer(8), Value::Blob(record([ Value::Text("index".to_string()), Value::Text("q_abc".to_string()), Value::Text("q".to_string()), - Value::Integer(8), + Value::Integer(9), Value::Text("CREATE INDEX q_abc ON q (a, b, c)".to_string()) ])), Value::Null, @@ -1074,13 +1074,13 @@ fn test_cdc_schema_changes_alter_table() { Value::Null, Value::Integer(1), Value::Text("sqlite_schema".to_string()), - Value::Integer(2), + Value::Integer(3), Value::Null, Value::Blob(record([ Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x, y, z UNIQUE, q, PRIMARY KEY (x, y))".to_string() ) @@ -1092,12 +1092,12 @@ fn test_cdc_schema_changes_alter_table() { Value::Null, Value::Integer(0), Value::Text("sqlite_schema".to_string()), - Value::Integer(2), + Value::Integer(3), Value::Blob(record([ Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x, y, z UNIQUE, q, PRIMARY KEY (x, y))".to_string() ) @@ -1106,7 +1106,7 @@ fn test_cdc_schema_changes_alter_table() { Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x PRIMARY KEY, y PRIMARY KEY, z UNIQUE)".to_string() ) @@ -1129,12 +1129,12 @@ fn test_cdc_schema_changes_alter_table() { Value::Null, Value::Integer(0), Value::Text("sqlite_schema".to_string()), - Value::Integer(2), + Value::Integer(3), Value::Blob(record([ Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x PRIMARY KEY, y PRIMARY KEY, z UNIQUE)".to_string() ) @@ -1143,7 +1143,7 @@ fn test_cdc_schema_changes_alter_table() { Value::Text("table".to_string()), Value::Text("t".to_string()), Value::Text("t".to_string()), - Value::Integer(3), + Value::Integer(4), Value::Text( "CREATE TABLE t (x PRIMARY KEY, y PRIMARY KEY, z UNIQUE, t)".to_string() ) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index b0a6bc1c1..f5e96dee2 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -55,7 +55,10 @@ mod tests { #[test] pub fn rowid_seek_fuzz() { - let db = TempDatabase::new_with_rusqlite("CREATE TABLE t (x INTEGER PRIMARY KEY)", false); // INTEGER PRIMARY KEY is a rowid alias, so an index is not created + let db = TempDatabase::new_with_rusqlite( + "CREATE TABLE t (x INTEGER PRIMARY KEY autoincrement)", + false, + ); // INTEGER PRIMARY KEY is a rowid alias, so an index is not created let sqlite_conn = rusqlite::Connection::open(db.path.clone()).unwrap(); let (mut rng, _seed) = rng_from_time_or_env(); @@ -525,10 +528,13 @@ mod tests { let limbo_db = TempDatabase::new_empty(true); let sqlite_db = TempDatabase::new_empty(true); let num_cols = rng.random_range(1..=10); - let table_def = (0..num_cols) - .map(|i| format!("c{i} INTEGER")) - .collect::>(); - let table_def = table_def.join(", "); + let mut table_cols = vec!["id INTEGER PRIMARY KEY AUTOINCREMENT".to_string()]; + table_cols.extend( + (0..num_cols) + .map(|i| format!("c{i} INTEGER")) + .collect::>(), + ); + let table_def = table_cols.join(", "); let table_def = format!("CREATE TABLE t ({table_def})"); let num_indexes = rng.random_range(0..=num_cols); @@ -572,7 +578,15 @@ mod tests { } // Track executed statements in case we fail let mut dml_statements = Vec::new(); - let insert = format!("INSERT INTO t VALUES {}", insert_values.join(", ")); + let col_names = (0..num_cols) + .map(|i| format!("c{i}")) + .collect::>() + .join(", "); + let insert = format!( + "INSERT INTO t ({}) VALUES {}", + col_names, + insert_values.join(", ") + ); dml_statements.push(insert.clone()); // Insert initial data into both databases @@ -692,7 +706,10 @@ mod tests { let num_cols = rng.random_range(2..=4); // We'll always include a TEXT "k" and a couple INT columns to give predicates variety. // Build: id INTEGER PRIMARY KEY, k TEXT, c0 INT, c1 INT, ... - let mut cols: Vec = vec!["id INTEGER PRIMARY KEY".into(), "k TEXT".into()]; + let mut cols: Vec = vec![ + "id INTEGER PRIMARY KEY AUTOINCREMENT".into(), + "k TEXT".into(), + ]; for i in 0..(num_cols - 1) { cols.push(format!("c{i} INT")); }