@@ -13,12 +13,12 @@ use crate::storage::database::DatabaseFile;
1313use crate::storage::journal_mode;
1414use crate::storage::page_cache::PageCache;
1515use crate::storage::pager::{default_page1, CreateBTreeFlags, PageRef};
16- use crate :: storage:: sqlite3_ondisk:: { DatabaseHeader , PageSize , RawVersion } ;
16+ use crate::storage::sqlite3_ondisk::{read_varint, DatabaseHeader, PageSize, RawVersion};
1717use crate::translate::collate::CollationSeq;
1818use crate::translate::pragma::TURSO_CDC_VERSION_TABLE_NAME;
1919use crate::types::{
2020 compare_immutable, compare_records_generic, AsValueRef, Extendable, IOCompletions, IOResult,
21- ImmutableRecord , IndexInfo , SeekResult , Text ,
21+ ImmutableRecord, IndexInfo, SeekResult, SerialType, SerialTypeKind, Text,
2222};
2323use crate::util::{
2424 normalize_ident, rewrite_check_expr_column_refs, rewrite_check_expr_table_refs,
@@ -9423,6 +9423,71 @@ fn format_integrity_check_result(errors: &[IntegrityCheckError]) -> String {
94239423 }
94249424}
94259425
9426+ fn index_record_rowid_at_end(record: &ImmutableRecord, index_info: &IndexInfo) -> Result<bool> {
9427+ if !index_info.has_rowid {
9428+ return Ok(true);
9429+ }
9430+
9431+ let payload = record.get_payload();
9432+ if payload.is_empty() {
9433+ return Err(LimboError::Corrupt("Index record payload is empty".into()));
9434+ }
9435+
9436+ let (header_size, header_varint_len) = read_varint(payload)?;
9437+ let header_size = header_size as usize;
9438+ if header_size > payload.len() || header_varint_len > payload.len() {
9439+ return Err(LimboError::Corrupt(
9440+ "Index record header exceeds payload size".into(),
9441+ ));
9442+ }
9443+
9444+ let mut header = &payload[header_varint_len..header_size];
9445+ let mut field_count = 0usize;
9446+ let mut data_size = 0usize;
9447+ let mut last_serial_type: Option<SerialType> = None;
9448+
9449+ while !header.is_empty() {
9450+ let (serial_type_raw, bytes_read) = read_varint(header)?;
9451+ header = &header[bytes_read..];
9452+
9453+ let serial_type = SerialType::try_from(serial_type_raw)?;
9454+ data_size = data_size
9455+ .checked_add(serial_type.size())
9456+ .ok_or_else(|| LimboError::Corrupt("Index record data size overflow".into()))?;
9457+ field_count += 1;
9458+ last_serial_type = Some(serial_type);
9459+ }
9460+
9461+ if field_count != index_info.num_cols {
9462+ return Err(LimboError::Corrupt(format!(
9463+ "Index record field count {} != expected {}",
9464+ field_count, index_info.num_cols
9465+ )));
9466+ }
9467+
9468+ let payload_data_len = payload.len().saturating_sub(header_size);
9469+ if data_size > payload_data_len {
9470+ return Err(LimboError::Corrupt(
9471+ "Index record data section too small".into(),
9472+ ));
9473+ }
9474+
9475+ let last_serial_type = last_serial_type
9476+ .ok_or_else(|| LimboError::Corrupt("Index record has no serial types".into()))?;
9477+
9478+ Ok(matches!(
9479+ last_serial_type.kind(),
9480+ SerialTypeKind::I8
9481+ | SerialTypeKind::I16
9482+ | SerialTypeKind::I24
9483+ | SerialTypeKind::I32
9484+ | SerialTypeKind::I48
9485+ | SerialTypeKind::I64
9486+ | SerialTypeKind::ConstInt0
9487+ | SerialTypeKind::ConstInt1
9488+ ))
9489+ }
9490+
94269491pub enum OpIntegrityCheckState {
94279492 Start,
94289493 /// Phase 1: B-tree structural checks
@@ -9431,7 +9496,20 @@ pub enum OpIntegrityCheckState {
94319496 current_root_idx: usize,
94329497 state: IntegrityCheckState,
94339498 },
9434- /// Phase 2: Index entry count validation (runs for both quick_check and integrity_check)
9499+ /// Phase 2: Index rowid position validation (skipped when quick=true)
9500+ /// Scans index entries and ensures the rowid is the last column in each record.
9501+ ValidatingIndexRowidPosition {
9502+ errors: Vec<IntegrityCheckError>,
9503+ /// Index into the tables list
9504+ current_table_idx: usize,
9505+ /// Index into the current table's indexes list
9506+ current_index_idx: usize,
9507+ /// Cursor used for index scanning (persisted across I/O yields)
9508+ cursor: Option<Box<BTreeCursor>>,
9509+ /// State of the current validation sub-phase
9510+ substate: IndexRowidValidationSubstate,
9511+ },
9512+ /// Phase 3: Index entry count validation (runs for both quick_check and integrity_check)
94359513 /// Counts rows in each table and entries in each index, then compares.
94369514 ValidatingIndexCounts {
94379515 errors: Vec<IntegrityCheckError>,
@@ -9448,7 +9526,7 @@ pub enum OpIntegrityCheckState {
94489526 /// Cursor used for counting (persisted across I/O yields)
94499527 cursor: Option<Box<BTreeCursor>>,
94509528 },
9451- /// Phase 3 : NOT NULL constraint validation (skipped when quick=true)
9529+ /// Phase 4 : NOT NULL constraint validation (skipped when quick=true)
94529530 /// Scans table rows and checks that NOT NULL columns don't contain NULL values.
94539531 ValidatingNotNull {
94549532 errors: Vec<IntegrityCheckError>,
@@ -9459,7 +9537,7 @@ pub enum OpIntegrityCheckState {
94599537 /// State of the current validation sub-phase
94609538 substate: NotNullValidationSubstate,
94619539 },
9462- /// Phase 4 : UNIQUE constraint validation (skipped when quick=true)
9540+ /// Phase 5 : UNIQUE constraint validation (skipped when quick=true)
94639541 /// Scans unique indexes and checks for duplicate keys.
94649542 ValidatingUnique {
94659543 errors: Vec<IntegrityCheckError>,
@@ -9475,7 +9553,7 @@ pub enum OpIntegrityCheckState {
94759553 /// State of the current validation sub-phase
94769554 substate: UniqueValidationSubstate,
94779555 },
9478- /// Phase 5 : Row-index consistency validation (skipped when quick=true)
9556+ /// Phase 6 : Row-index consistency validation (skipped when quick=true)
94799557 /// For each table row, verifies it has a corresponding entry in each index.
94809558 ValidatingRowIndexConsistency {
94819559 errors: Vec<IntegrityCheckError>,
@@ -9507,6 +9585,21 @@ pub enum IndexCountValidationSubstate {
95079585 ComparingCounts,
95089586}
95099587
9588+ /// Sub-states for index rowid position validation
9589+ #[derive(Debug, Clone)]
9590+ pub enum IndexRowidValidationSubstate {
9591+ /// Starting validation for a new table
9592+ StartTable,
9593+ /// Starting validation for a new index
9594+ StartIndex,
9595+ /// Rewinding to the beginning of the index
9596+ Rewinding,
9597+ /// Checking rowid position in the current index entry
9598+ CheckingEntry,
9599+ /// Advancing to the next entry
9600+ Advancing,
9601+ }
9602+
95109603/// Sub-states for NOT NULL constraint validation
95119604#[derive(Debug, Clone)]
95129605pub enum NotNullValidationSubstate {
@@ -9693,7 +9786,25 @@ pub fn op_integrity_check(
96939786 }
96949787 }
96959788
9696- // Phase 2: Index count validation (skip for quick_check per SQLite)
9789+ // Phase 2: Index rowid position validation (skip for quick_check per SQLite)
9790+ if !*quick
9791+ && tables
9792+ .iter()
9793+ .any(|t| t.indexes.iter().any(|idx| idx.index_info.has_rowid))
9794+ {
9795+ let errors = std::mem::take(errors);
9796+ state.op_integrity_check_state =
9797+ OpIntegrityCheckState::ValidatingIndexRowidPosition {
9798+ errors,
9799+ current_table_idx: 0,
9800+ current_index_idx: 0,
9801+ cursor: None,
9802+ substate: IndexRowidValidationSubstate::StartTable,
9803+ };
9804+ return Ok(InsnFunctionStepResult::Step);
9805+ }
9806+
9807+ // Phase 3: Index count validation (skip for quick_check per SQLite)
96979808 if !*quick && tables.iter().any(|t| !t.indexes.is_empty()) {
96989809 // Transition to index count validation phase
96999810 let errors = std::mem::take(errors);
@@ -9709,7 +9820,7 @@ pub fn op_integrity_check(
97099820 return Ok(InsnFunctionStepResult::Step);
97109821 }
97119822
9712- // Phase 3 : NOT NULL constraint validation (skip for quick_check per SQLite)
9823+ // Phase 4 : NOT NULL constraint validation (skip for quick_check per SQLite)
97139824 if !*quick && tables.iter().any(|t| !t.not_null_columns.is_empty()) {
97149825 // Transition to NOT NULL validation phase
97159826 let errors = std::mem::take(errors);
@@ -9722,7 +9833,7 @@ pub fn op_integrity_check(
97229833 return Ok(InsnFunctionStepResult::Step);
97239834 }
97249835
9725- // Phase 4 : UNIQUE constraint validation (skip if quick_check)
9836+ // Phase 5 : UNIQUE constraint validation (skip if quick_check)
97269837 if !*quick
97279838 && tables
97289839 .iter()
@@ -9747,6 +9858,136 @@ pub fn op_integrity_check(
97479858 state.pc += 1;
97489859 }
97499860 }
9861+ OpIntegrityCheckState::ValidatingIndexRowidPosition {
9862+ errors,
9863+ current_table_idx,
9864+ current_index_idx,
9865+ cursor,
9866+ substate,
9867+ } => {
9868+ loop {
9869+ match substate {
9870+ IndexRowidValidationSubstate::StartTable => {
9871+ // Skip tables without indexes that include rowid
9872+ while *current_table_idx < tables.len()
9873+ && tables[*current_table_idx]
9874+ .indexes
9875+ .iter()
9876+ .all(|idx| !idx.index_info.has_rowid)
9877+ {
9878+ *current_table_idx += 1;
9879+ }
9880+
9881+ if *current_table_idx >= tables.len() {
9882+ // Done scanning rowid positions; transition to index count validation
9883+ let errors = std::mem::take(errors);
9884+ state.op_integrity_check_state =
9885+ OpIntegrityCheckState::ValidatingIndexCounts {
9886+ errors,
9887+ current_table_idx: 0,
9888+ current_index_idx: 0,
9889+ table_row_count: 0,
9890+ index_entry_count: 0,
9891+ substate: IndexCountValidationSubstate::StartTable,
9892+ cursor: None,
9893+ };
9894+ return Ok(InsnFunctionStepResult::Step);
9895+ }
9896+
9897+ *current_index_idx = 0;
9898+ *substate = IndexRowidValidationSubstate::StartIndex;
9899+ }
9900+ IndexRowidValidationSubstate::StartIndex => {
9901+ let table = &tables[*current_table_idx];
9902+
9903+ while *current_index_idx < table.indexes.len()
9904+ && !table.indexes[*current_index_idx].index_info.has_rowid
9905+ {
9906+ *current_index_idx += 1;
9907+ }
9908+
9909+ if *current_index_idx >= table.indexes.len() {
9910+ // Done with this table, move to next
9911+ *current_table_idx += 1;
9912+ *substate = IndexRowidValidationSubstate::StartTable;
9913+ continue;
9914+ }
9915+
9916+ let index = &table.indexes[*current_index_idx];
9917+ *cursor = Some(Box::new(BTreeCursor::new(
9918+ pager.clone(),
9919+ index.root_page,
9920+ 0,
9921+ )));
9922+ *substate = IndexRowidValidationSubstate::Rewinding;
9923+ }
9924+ IndexRowidValidationSubstate::Rewinding => {
9925+ let btree_cursor = cursor.as_mut().unwrap();
9926+ return_if_io!(btree_cursor.rewind());
9927+ if !btree_cursor.has_record() {
9928+ // Index is empty, move to next index
9929+ *cursor = None;
9930+ *current_index_idx += 1;
9931+ *substate = IndexRowidValidationSubstate::StartIndex;
9932+ continue;
9933+ }
9934+ *substate = IndexRowidValidationSubstate::CheckingEntry;
9935+ }
9936+ IndexRowidValidationSubstate::CheckingEntry => {
9937+ let btree_cursor = cursor.as_mut().unwrap();
9938+ let record = return_if_io!(btree_cursor.record())
9939+ .expect("record must exist when has_record() is true");
9940+ let index = &tables[*current_table_idx].indexes[*current_index_idx];
9941+ match index_record_rowid_at_end(record, &index.index_info) {
9942+ Ok(true) => {}
9943+ Ok(false) => {
9944+ errors.push(IntegrityCheckError::RowidNotAtEndOfRecord {
9945+ index_name: index.name.clone(),
9946+ });
9947+ }
9948+ Err(err) => {
9949+ errors.push(IntegrityCheckError::IndexRecordCorrupt {
9950+ index_name: index.name.clone(),
9951+ error: err.to_string(),
9952+ });
9953+
9954+ if errors.len() >= *max_errors {
9955+ let message = format_integrity_check_result(errors);
9956+ state.registers[*message_register] =
9957+ Register::Value(Value::build_text(message));
9958+ state.op_integrity_check_state = OpIntegrityCheckState::Start;
9959+ state.pc += 1;
9960+ return Ok(InsnFunctionStepResult::Step);
9961+ }
9962+ }
9963+ }
9964+
9965+ if errors.len() >= *max_errors {
9966+ let message = format_integrity_check_result(errors);
9967+ state.registers[*message_register] =
9968+ Register::Value(Value::build_text(message));
9969+ state.op_integrity_check_state = OpIntegrityCheckState::Start;
9970+ state.pc += 1;
9971+ return Ok(InsnFunctionStepResult::Step);
9972+ }
9973+
9974+ *substate = IndexRowidValidationSubstate::Advancing;
9975+ }
9976+ IndexRowidValidationSubstate::Advancing => {
9977+ let btree_cursor = cursor.as_mut().unwrap();
9978+ return_if_io!(btree_cursor.next());
9979+ if btree_cursor.has_record() {
9980+ *substate = IndexRowidValidationSubstate::CheckingEntry;
9981+ } else {
9982+ // Done with this index, move to next
9983+ *cursor = None;
9984+ *current_index_idx += 1;
9985+ *substate = IndexRowidValidationSubstate::StartIndex;
9986+ }
9987+ }
9988+ }
9989+ }
9990+ }
97509991 OpIntegrityCheckState::ValidatingIndexCounts {
97519992 errors,
97529993 current_table_idx,
@@ -9770,7 +10011,7 @@ pub fn op_integrity_check(
977010011 // Done with all tables, transition to NOT NULL validation, UNIQUE validation,
977110012 // row-index validation, or finish
977210013
9773- // Phase 3 : NOT NULL constraint validation (skip for quick_check)
10014+ // Phase 4 : NOT NULL constraint validation (skip for quick_check)
977410015 if !*quick && tables.iter().any(|t| !t.not_null_columns.is_empty()) {
977510016 let errors = std::mem::take(errors);
977610017 state.op_integrity_check_state =
@@ -9783,7 +10024,7 @@ pub fn op_integrity_check(
978310024 return Ok(InsnFunctionStepResult::Step);
978410025 }
978510026
9786- // Phase 4 : UNIQUE constraint validation (skip for quick_check)
10027+ // Phase 5 : UNIQUE constraint validation (skip for quick_check)
978710028 if !*quick
978810029 && tables
978910030 .iter()
@@ -9802,7 +10043,7 @@ pub fn op_integrity_check(
980210043 return Ok(InsnFunctionStepResult::Step);
980310044 }
980410045
9805- // Phase 5 : Row-index consistency validation (skip if quick_check)
10046+ // Phase 6 : Row-index consistency validation (skip if quick_check)
980610047 if !*quick && tables.iter().any(|t| !t.indexes.is_empty()) {
980710048 let errors = std::mem::take(errors);
980810049 state.op_integrity_check_state =
@@ -9917,7 +10158,7 @@ pub fn op_integrity_check(
991710158 if *current_table_idx >= tables.len() {
991810159 // Done with all tables, transition to UNIQUE validation, row-index validation, or finish
991910160
9920- // Phase 4 : UNIQUE constraint validation (skip for quick_check)
10161+ // Phase 5 : UNIQUE constraint validation (skip for quick_check)
992110162 // Note: We can only be in ValidatingNotNull if quick=false, but check anyway for clarity
992210163 if !*quick
992310164 && tables
@@ -9937,7 +10178,7 @@ pub fn op_integrity_check(
993710178 return Ok(InsnFunctionStepResult::Step);
993810179 }
993910180
9940- // Phase 5 : Row-index consistency validation (skip if quick_check)
10181+ // Phase 6 : Row-index consistency validation (skip if quick_check)
994110182 if !*quick && tables.iter().any(|t| !t.indexes.is_empty()) {
994210183 let errors = std::mem::take(errors);
994310184 state.op_integrity_check_state =
0 commit comments