Skip to main content

fsqlite_ext_session/
lib.rs

1use fsqlite_types::serial_type::{read_varint, varint_len, write_varint};
2use fsqlite_types::value::SqliteValue;
3
4// ---------------------------------------------------------------------------
5// Constants
6// ---------------------------------------------------------------------------
7
8/// Table header marker byte ('T').
9const TABLE_HEADER_BYTE: u8 = 0x54;
10
11/// Operation codes used in changeset/patchset binary format.
12const OP_INSERT: u8 = 0x12; // 18
13const OP_DELETE: u8 = 0x09; // 9
14const OP_UPDATE: u8 = 0x17; // 23
15
16/// Value type markers in the changeset binary format.
17const VAL_UNDEFINED: u8 = 0x00;
18const VAL_INTEGER: u8 = 0x01;
19const VAL_REAL: u8 = 0x02;
20const VAL_TEXT: u8 = 0x03;
21const VAL_BLOB: u8 = 0x04;
22const VAL_NULL: u8 = 0x05;
23
24// ---------------------------------------------------------------------------
25// Public API — extension name
26// ---------------------------------------------------------------------------
27
28#[must_use]
29pub const fn extension_name() -> &'static str {
30    "session"
31}
32
33// ---------------------------------------------------------------------------
34// Change operations
35// ---------------------------------------------------------------------------
36
37/// The kind of DML operation recorded in a changeset.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ChangeOp {
40    Insert,
41    Delete,
42    Update,
43}
44
45impl ChangeOp {
46    #[must_use]
47    pub const fn as_byte(self) -> u8 {
48        match self {
49            Self::Insert => OP_INSERT,
50            Self::Delete => OP_DELETE,
51            Self::Update => OP_UPDATE,
52        }
53    }
54
55    /// Decode an operation byte from the changeset format.
56    ///
57    /// Returns `None` for unrecognised bytes.
58    #[must_use]
59    pub const fn from_byte(b: u8) -> Option<Self> {
60        match b {
61            OP_INSERT => Some(Self::Insert),
62            OP_DELETE => Some(Self::Delete),
63            OP_UPDATE => Some(Self::Update),
64            _ => None,
65        }
66    }
67}
68
69// ---------------------------------------------------------------------------
70// Conflict types and actions
71// ---------------------------------------------------------------------------
72
73/// The category of conflict encountered while applying a changeset.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum ConflictType {
76    /// The row exists but its current values differ from the expected old values.
77    Data,
78    /// The row to update or delete does not exist in the target database.
79    NotFound,
80    /// A unique-constraint violation occurred (e.g. duplicate key on INSERT).
81    Conflict,
82    /// A non-unique constraint violation occurred (CHECK, NOT NULL, etc.).
83    Constraint,
84    /// A foreign-key constraint violation occurred.
85    ForeignKey,
86}
87
88/// The action the caller wants the apply engine to take for a conflict.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum ConflictAction {
91    /// Skip this change and continue applying the rest of the changeset.
92    OmitChange,
93    /// Overwrite the conflicting row with the incoming change.
94    Replace,
95    /// Abort the entire apply operation immediately.
96    Abort,
97}
98
99// ---------------------------------------------------------------------------
100// Changeset value encoding / decoding
101// ---------------------------------------------------------------------------
102
103/// A single column value in the changeset binary format.
104///
105/// `Undefined` is used in UPDATE records for columns that did not change.
106#[derive(Debug, Clone, PartialEq)]
107pub enum ChangesetValue {
108    Undefined,
109    Null,
110    Integer(i64),
111    Real(f64),
112    Text(String),
113    Blob(Vec<u8>),
114}
115
116impl ChangesetValue {
117    /// Convert from a [`SqliteValue`].
118    #[must_use]
119    pub fn from_sqlite(val: &SqliteValue) -> Self {
120        match val {
121            SqliteValue::Null => Self::Null,
122            SqliteValue::Integer(i) => Self::Integer(*i),
123            SqliteValue::Float(f) => Self::Real(*f),
124            SqliteValue::Text(s) => Self::Text(s.clone()),
125            SqliteValue::Blob(b) => Self::Blob(b.clone()),
126        }
127    }
128
129    /// Convert to a [`SqliteValue`], mapping `Undefined` to `Null`.
130    #[must_use]
131    pub fn to_sqlite(&self) -> SqliteValue {
132        match self {
133            Self::Undefined | Self::Null => SqliteValue::Null,
134            Self::Integer(i) => SqliteValue::Integer(*i),
135            Self::Real(f) => SqliteValue::Float(*f),
136            Self::Text(s) => SqliteValue::Text(s.clone()),
137            Self::Blob(b) => SqliteValue::Blob(b.clone()),
138        }
139    }
140
141    /// Encode this value into the changeset binary format, appending to `out`.
142    pub fn encode(&self, out: &mut Vec<u8>) {
143        match self {
144            Self::Undefined => {
145                out.push(VAL_UNDEFINED);
146            }
147            Self::Null => {
148                out.push(VAL_NULL);
149            }
150            Self::Integer(i) => {
151                out.push(VAL_INTEGER);
152                out.extend_from_slice(&i.to_be_bytes());
153            }
154            Self::Real(f) => {
155                out.push(VAL_REAL);
156                out.extend_from_slice(&f.to_be_bytes());
157            }
158            Self::Text(s) => {
159                out.push(VAL_TEXT);
160                let bytes = s.as_bytes();
161                let mut vbuf = [0u8; 9];
162                let vlen = write_varint(&mut vbuf, bytes.len() as u64);
163                out.extend_from_slice(&vbuf[..vlen]);
164                out.extend_from_slice(bytes);
165            }
166            Self::Blob(b) => {
167                out.push(VAL_BLOB);
168                let mut vbuf = [0u8; 9];
169                let vlen = write_varint(&mut vbuf, b.len() as u64);
170                out.extend_from_slice(&vbuf[..vlen]);
171                out.extend_from_slice(b);
172            }
173        }
174    }
175
176    /// Decode a single value from `data` starting at `pos`.
177    ///
178    /// Returns `(value, bytes_consumed)` or `None` on malformed input.
179    pub fn decode(data: &[u8], pos: usize) -> Option<(Self, usize)> {
180        let type_byte = *data.get(pos)?;
181        let mut offset = pos + 1;
182        match type_byte {
183            VAL_UNDEFINED => Some((Self::Undefined, offset - pos)),
184            VAL_NULL => Some((Self::Null, offset - pos)),
185            VAL_INTEGER => {
186                let end = offset + 8;
187                if data.len() < end {
188                    return None;
189                }
190                let arr: [u8; 8] = data[offset..end].try_into().ok()?;
191                Some((Self::Integer(i64::from_be_bytes(arr)), end - pos))
192            }
193            VAL_REAL => {
194                let end = offset + 8;
195                if data.len() < end {
196                    return None;
197                }
198                let arr: [u8; 8] = data[offset..end].try_into().ok()?;
199                Some((Self::Real(f64::from_be_bytes(arr)), end - pos))
200            }
201            VAL_TEXT => {
202                let (len, vlen) = read_varint(&data[offset..])?;
203                offset += vlen;
204                let len = usize::try_from(len).ok()?;
205                let end = offset + len;
206                if data.len() < end {
207                    return None;
208                }
209                let s = std::str::from_utf8(&data[offset..end]).ok()?;
210                Some((Self::Text(s.to_owned()), end - pos))
211            }
212            VAL_BLOB => {
213                let (len, vlen) = read_varint(&data[offset..])?;
214                offset += vlen;
215                let len = usize::try_from(len).ok()?;
216                let end = offset + len;
217                if data.len() < end {
218                    return None;
219                }
220                Some((Self::Blob(data[offset..end].to_vec()), end - pos))
221            }
222            _ => None,
223        }
224    }
225}
226
227// ---------------------------------------------------------------------------
228// Table info carried in the changeset
229// ---------------------------------------------------------------------------
230
231/// Per-table metadata stored in the changeset header.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct TableInfo {
234    /// Table name.
235    pub name: String,
236    /// Number of columns.
237    pub column_count: usize,
238    /// For each column, `true` if it is part of the primary key.
239    pub pk_flags: Vec<bool>,
240}
241
242impl TableInfo {
243    /// Encode the table header into changeset binary format.
244    pub fn encode(&self, out: &mut Vec<u8>) {
245        out.push(TABLE_HEADER_BYTE);
246        let mut vbuf = [0u8; 9];
247        let vlen = write_varint(&mut vbuf, self.column_count as u64);
248        out.extend_from_slice(&vbuf[..vlen]);
249        for &pk in &self.pk_flags {
250            out.push(u8::from(pk));
251        }
252        out.extend_from_slice(self.name.as_bytes());
253        out.push(0x00); // NUL terminator
254    }
255
256    /// Decode a table header starting at `pos`.
257    ///
258    /// Returns `(TableInfo, bytes_consumed)` or `None`.
259    pub fn decode(data: &[u8], pos: usize) -> Option<(Self, usize)> {
260        if *data.get(pos)? != TABLE_HEADER_BYTE {
261            return None;
262        }
263        let mut offset = pos + 1;
264        let (col_count, vlen) = read_varint(&data[offset..])?;
265        offset += vlen;
266        let col_count = usize::try_from(col_count).ok()?;
267        if data.len() < offset + col_count {
268            return None;
269        }
270        let pk_flags: Vec<bool> = data[offset..offset + col_count]
271            .iter()
272            .map(|&b| b != 0)
273            .collect();
274        offset += col_count;
275        // Read NUL-terminated table name.
276        let name_start = offset;
277        let nul_pos = data[name_start..].iter().position(|&b| b == 0)?;
278        let name = std::str::from_utf8(&data[name_start..name_start + nul_pos])
279            .ok()?
280            .to_owned();
281        offset = name_start + nul_pos + 1;
282        Some((
283            Self {
284                name,
285                column_count: col_count,
286                pk_flags,
287            },
288            offset - pos,
289        ))
290    }
291}
292
293// ---------------------------------------------------------------------------
294// Change row
295// ---------------------------------------------------------------------------
296
297/// A single row change recorded in a changeset.
298#[derive(Debug, Clone, PartialEq)]
299pub struct ChangesetRow {
300    pub op: ChangeOp,
301    /// For DELETE and UPDATE: the old column values. Empty for INSERT.
302    pub old_values: Vec<ChangesetValue>,
303    /// For INSERT and UPDATE: the new column values. Empty for DELETE.
304    pub new_values: Vec<ChangesetValue>,
305}
306
307impl ChangesetRow {
308    /// Encode this row change into changeset binary format.
309    pub fn encode_changeset(&self, out: &mut Vec<u8>) {
310        out.push(self.op.as_byte());
311        match self.op {
312            ChangeOp::Insert => {
313                for v in &self.new_values {
314                    v.encode(out);
315                }
316            }
317            ChangeOp::Delete => {
318                for v in &self.old_values {
319                    v.encode(out);
320                }
321            }
322            ChangeOp::Update => {
323                for v in &self.old_values {
324                    v.encode(out);
325                }
326                for v in &self.new_values {
327                    v.encode(out);
328                }
329            }
330        }
331    }
332
333    /// Encode this row change into patchset binary format.
334    ///
335    /// For INSERT and DELETE this is identical to changeset encoding.
336    /// For UPDATE the old values are omitted — only PK columns (from old
337    /// values) plus new values are written.
338    pub fn encode_patchset(&self, out: &mut Vec<u8>, pk_flags: &[bool]) {
339        out.push(self.op.as_byte());
340        match self.op {
341            ChangeOp::Insert => {
342                for v in &self.new_values {
343                    v.encode(out);
344                }
345            }
346            ChangeOp::Delete => {
347                for v in &self.old_values {
348                    v.encode(out);
349                }
350            }
351            ChangeOp::Update => {
352                // Patchset UPDATE: emit PK old values only, then new values.
353                for (i, v) in self.old_values.iter().enumerate() {
354                    if pk_flags.get(i).copied().unwrap_or(false) {
355                        v.encode(out);
356                    }
357                }
358                for v in &self.new_values {
359                    v.encode(out);
360                }
361            }
362        }
363    }
364
365    /// Decode one changeset row starting at `pos`, given the column count.
366    pub fn decode_changeset(data: &[u8], pos: usize, col_count: usize) -> Option<(Self, usize)> {
367        let op = ChangeOp::from_byte(*data.get(pos)?)?;
368        let mut offset = pos + 1;
369
370        let decode_n = |data: &[u8], offset: &mut usize, n: usize| -> Option<Vec<ChangesetValue>> {
371            let mut vals = Vec::with_capacity(n);
372            for _ in 0..n {
373                let (v, consumed) = ChangesetValue::decode(data, *offset)?;
374                *offset += consumed;
375                vals.push(v);
376            }
377            Some(vals)
378        };
379
380        let (old_values, new_values) = match op {
381            ChangeOp::Insert => {
382                let new_values = decode_n(data, &mut offset, col_count)?;
383                (Vec::new(), new_values)
384            }
385            ChangeOp::Delete => {
386                let old_values = decode_n(data, &mut offset, col_count)?;
387                (old_values, Vec::new())
388            }
389            ChangeOp::Update => {
390                let old_values = decode_n(data, &mut offset, col_count)?;
391                let new_values = decode_n(data, &mut offset, col_count)?;
392                (old_values, new_values)
393            }
394        };
395
396        Some((
397            Self {
398                op,
399                old_values,
400                new_values,
401            },
402            offset - pos,
403        ))
404    }
405
406    /// Invert this change: INSERT becomes DELETE, DELETE becomes INSERT,
407    /// UPDATE swaps old and new values.
408    #[must_use]
409    pub fn invert(&self) -> Self {
410        match self.op {
411            ChangeOp::Insert => Self {
412                op: ChangeOp::Delete,
413                old_values: self.new_values.clone(),
414                new_values: Vec::new(),
415            },
416            ChangeOp::Delete => Self {
417                op: ChangeOp::Insert,
418                old_values: Vec::new(),
419                new_values: self.old_values.clone(),
420            },
421            ChangeOp::Update => Self {
422                op: ChangeOp::Update,
423                old_values: self.new_values.clone(),
424                new_values: self.old_values.clone(),
425            },
426        }
427    }
428}
429
430// ---------------------------------------------------------------------------
431// Per-table changeset section
432// ---------------------------------------------------------------------------
433
434/// All row changes for a single table within a changeset.
435#[derive(Debug, Clone, PartialEq)]
436pub struct TableChangeset {
437    pub info: TableInfo,
438    pub rows: Vec<ChangesetRow>,
439}
440
441impl TableChangeset {
442    /// Encode this table section in changeset format.
443    pub fn encode_changeset(&self, out: &mut Vec<u8>) {
444        self.info.encode(out);
445        for row in &self.rows {
446            row.encode_changeset(out);
447        }
448    }
449
450    /// Encode this table section in patchset format.
451    pub fn encode_patchset(&self, out: &mut Vec<u8>) {
452        self.info.encode(out);
453        for row in &self.rows {
454            row.encode_patchset(out, &self.info.pk_flags);
455        }
456    }
457}
458
459// ---------------------------------------------------------------------------
460// Full changeset
461// ---------------------------------------------------------------------------
462
463/// A complete changeset covering one or more tables.
464#[derive(Debug, Clone, PartialEq)]
465pub struct Changeset {
466    pub tables: Vec<TableChangeset>,
467}
468
469impl Changeset {
470    /// Create an empty changeset.
471    #[must_use]
472    pub fn new() -> Self {
473        Self { tables: Vec::new() }
474    }
475
476    /// Encode the entire changeset in binary format.
477    #[must_use]
478    pub fn encode(&self) -> Vec<u8> {
479        let mut out = Vec::new();
480        for tc in &self.tables {
481            tc.encode_changeset(&mut out);
482        }
483        out
484    }
485
486    /// Encode the entire changeset as a patchset (compact form).
487    #[must_use]
488    pub fn encode_patchset(&self) -> Vec<u8> {
489        let mut out = Vec::new();
490        for tc in &self.tables {
491            tc.encode_patchset(&mut out);
492        }
493        out
494    }
495
496    /// Decode a changeset from its binary representation.
497    pub fn decode(data: &[u8]) -> Option<Self> {
498        let mut tables = Vec::new();
499        let mut pos = 0;
500        while pos < data.len() {
501            let (info, consumed) = TableInfo::decode(data, pos)?;
502            pos += consumed;
503            let mut rows = Vec::new();
504            // Read rows until we hit another table header or end of data.
505            while pos < data.len() && data[pos] != TABLE_HEADER_BYTE {
506                let (row, consumed) = ChangesetRow::decode_changeset(data, pos, info.column_count)?;
507                pos += consumed;
508                rows.push(row);
509            }
510            tables.push(TableChangeset { info, rows });
511        }
512        Some(Self { tables })
513    }
514
515    /// Invert the changeset: every INSERT becomes DELETE, every DELETE
516    /// becomes INSERT, every UPDATE swaps old and new values.
517    #[must_use]
518    pub fn invert(&self) -> Self {
519        Self {
520            tables: self
521                .tables
522                .iter()
523                .map(|tc| TableChangeset {
524                    info: tc.info.clone(),
525                    rows: tc.rows.iter().map(ChangesetRow::invert).collect(),
526                })
527                .collect(),
528        }
529    }
530
531    /// Concatenate another changeset onto this one.
532    pub fn concat(&mut self, other: &Self) {
533        for tc in &other.tables {
534            self.tables.push(tc.clone());
535        }
536    }
537}
538
539impl Default for Changeset {
540    fn default() -> Self {
541        Self::new()
542    }
543}
544
545// ---------------------------------------------------------------------------
546// Session — change tracker
547// ---------------------------------------------------------------------------
548
549/// A recorded change entry tracked by a [`Session`].
550#[derive(Debug, Clone)]
551struct TrackedChange {
552    table_name: String,
553    op: ChangeOp,
554    old_values: Vec<ChangesetValue>,
555    new_values: Vec<ChangesetValue>,
556}
557
558/// Metadata about a table being tracked by a [`Session`].
559#[derive(Debug, Clone)]
560struct TrackedTable {
561    name: String,
562    column_count: usize,
563    pk_flags: Vec<bool>,
564}
565
566/// A session that records database changes for later extraction as a
567/// changeset or patchset.
568///
569/// In a real database engine this would hook into the DML pipeline. For now
570/// it provides a programmatic API for recording changes and generating the
571/// binary changeset/patchset encoding.
572#[derive(Debug)]
573pub struct Session {
574    tables: Vec<TrackedTable>,
575    changes: Vec<TrackedChange>,
576}
577
578impl Session {
579    /// Create a new, empty session.
580    #[must_use]
581    pub fn new() -> Self {
582        Self {
583            tables: Vec::new(),
584            changes: Vec::new(),
585        }
586    }
587
588    /// Attach a table for change tracking.
589    ///
590    /// `pk_flags` indicates which columns are part of the primary key.
591    pub fn attach_table(&mut self, name: &str, column_count: usize, pk_flags: Vec<bool>) {
592        assert_eq!(
593            pk_flags.len(),
594            column_count,
595            "pk_flags length must match column_count"
596        );
597        self.tables.push(TrackedTable {
598            name: name.to_owned(),
599            column_count,
600            pk_flags,
601        });
602    }
603
604    /// Record an INSERT operation.
605    pub fn record_insert(&mut self, table: &str, new_values: Vec<ChangesetValue>) {
606        self.changes.push(TrackedChange {
607            table_name: table.to_owned(),
608            op: ChangeOp::Insert,
609            old_values: Vec::new(),
610            new_values,
611        });
612    }
613
614    /// Record a DELETE operation.
615    pub fn record_delete(&mut self, table: &str, old_values: Vec<ChangesetValue>) {
616        self.changes.push(TrackedChange {
617            table_name: table.to_owned(),
618            op: ChangeOp::Delete,
619            old_values,
620            new_values: Vec::new(),
621        });
622    }
623
624    /// Record an UPDATE operation.
625    ///
626    /// `old_values` and `new_values` must have the same length. Use
627    /// [`ChangesetValue::Undefined`] for columns that did not change.
628    pub fn record_update(
629        &mut self,
630        table: &str,
631        old_values: Vec<ChangesetValue>,
632        new_values: Vec<ChangesetValue>,
633    ) {
634        self.changes.push(TrackedChange {
635            table_name: table.to_owned(),
636            op: ChangeOp::Update,
637            old_values,
638            new_values,
639        });
640    }
641
642    /// Generate a [`Changeset`] from all recorded changes.
643    #[must_use]
644    pub fn changeset(&self) -> Changeset {
645        self.build_changeset_impl()
646    }
647
648    /// Generate a patchset (compact binary format).
649    #[must_use]
650    pub fn patchset(&self) -> Vec<u8> {
651        let cs = self.build_changeset_impl();
652        cs.encode_patchset()
653    }
654
655    /// Internal: collate tracked changes into per-table changeset sections.
656    fn build_changeset_impl(&self) -> Changeset {
657        let mut table_map: std::collections::HashMap<String, Vec<ChangesetRow>> =
658            std::collections::HashMap::new();
659
660        for change in &self.changes {
661            table_map
662                .entry(change.table_name.clone())
663                .or_default()
664                .push(ChangesetRow {
665                    op: change.op,
666                    old_values: change.old_values.clone(),
667                    new_values: change.new_values.clone(),
668                });
669        }
670
671        let mut tables = Vec::new();
672        // Emit tables in the order they were attached (deterministic).
673        for tracked in &self.tables {
674            if let Some(rows) = table_map.remove(&tracked.name) {
675                tables.push(TableChangeset {
676                    info: TableInfo {
677                        name: tracked.name.clone(),
678                        column_count: tracked.column_count,
679                        pk_flags: tracked.pk_flags.clone(),
680                    },
681                    rows,
682                });
683            }
684        }
685        // Any changes to tables not explicitly attached are appended with
686        // inferred metadata (all columns non-PK, count from first row).
687        for (name, rows) in table_map {
688            let col_count = rows.first().map_or(0, |r| {
689                if r.new_values.is_empty() {
690                    r.old_values.len()
691                } else {
692                    r.new_values.len()
693                }
694            });
695            tables.push(TableChangeset {
696                info: TableInfo {
697                    name,
698                    column_count: col_count,
699                    pk_flags: vec![false; col_count],
700                },
701                rows,
702            });
703        }
704        Changeset { tables }
705    }
706}
707
708impl Default for Session {
709    fn default() -> Self {
710        Self::new()
711    }
712}
713
714// ---------------------------------------------------------------------------
715// Changeset application
716// ---------------------------------------------------------------------------
717
718/// Outcome of applying a changeset to a target dataset.
719#[derive(Debug, Clone, PartialEq, Eq)]
720pub enum ApplyOutcome {
721    /// All changes were applied (some may have been skipped via `OmitChange`).
722    Success { applied: usize, skipped: usize },
723    /// The apply was aborted by the conflict handler.
724    Aborted { applied: usize },
725}
726
727/// A simple in-memory "database" for testing changeset application.
728///
729/// Maps `table_name -> Vec<row>` where each row is `Vec<SqliteValue>`.
730/// This is intentionally minimal; the real apply engine would operate on
731/// the B-tree layer.
732#[derive(Debug, Clone, Default)]
733pub struct SimpleTarget {
734    pub tables: std::collections::HashMap<String, Vec<Vec<SqliteValue>>>,
735}
736
737/// Result of applying a single row change: `Ok(applied)` or `Err(applied)`
738/// meaning abort with that many previously applied rows.
739type RowApplyResult = Result<bool, usize>;
740
741impl SimpleTarget {
742    /// Apply a changeset to this target, using `handler` for conflict
743    /// resolution.
744    pub fn apply<F>(&mut self, changeset: &Changeset, mut handler: F) -> ApplyOutcome
745    where
746        F: FnMut(ConflictType, &ChangesetRow) -> ConflictAction,
747    {
748        let mut applied = 0usize;
749        let mut skipped = 0usize;
750
751        for tc in &changeset.tables {
752            let rows = self.tables.entry(tc.info.name.clone()).or_default();
753            for change in &tc.rows {
754                let result = match change.op {
755                    ChangeOp::Insert => {
756                        Self::apply_insert(rows, &tc.info.pk_flags, change, &mut handler, applied)
757                    }
758                    ChangeOp::Delete => {
759                        Self::apply_delete(rows, &tc.info.pk_flags, change, &mut handler, applied)
760                    }
761                    ChangeOp::Update => {
762                        Self::apply_update(rows, &tc.info.pk_flags, change, &mut handler, applied)
763                    }
764                };
765                match result {
766                    Ok(true) => applied += 1,
767                    Ok(false) => skipped += 1,
768                    Err(n) => return ApplyOutcome::Aborted { applied: n },
769                }
770            }
771        }
772        ApplyOutcome::Success { applied, skipped }
773    }
774
775    fn apply_insert<F>(
776        rows: &mut Vec<Vec<SqliteValue>>,
777        pk_flags: &[bool],
778        change: &ChangesetRow,
779        handler: &mut F,
780        applied: usize,
781    ) -> RowApplyResult
782    where
783        F: FnMut(ConflictType, &ChangesetRow) -> ConflictAction,
784    {
785        let new_row: Vec<SqliteValue> = change
786            .new_values
787            .iter()
788            .map(ChangesetValue::to_sqlite)
789            .collect();
790        if Self::find_row_by_pk(rows, pk_flags, &new_row).is_some() {
791            match handler(ConflictType::Conflict, change) {
792                ConflictAction::OmitChange => return Ok(false),
793                ConflictAction::Replace => {
794                    let idx =
795                        Self::find_row_by_pk(rows, pk_flags, &new_row).expect("row just found");
796                    rows[idx] = new_row;
797                    return Ok(true);
798                }
799                ConflictAction::Abort => return Err(applied),
800            }
801        }
802        rows.push(new_row);
803        Ok(true)
804    }
805
806    fn apply_delete<F>(
807        rows: &mut Vec<Vec<SqliteValue>>,
808        pk_flags: &[bool],
809        change: &ChangesetRow,
810        handler: &mut F,
811        applied: usize,
812    ) -> RowApplyResult
813    where
814        F: FnMut(ConflictType, &ChangesetRow) -> ConflictAction,
815    {
816        let old_row: Vec<SqliteValue> = change
817            .old_values
818            .iter()
819            .map(ChangesetValue::to_sqlite)
820            .collect();
821        if let Some(idx) = Self::find_row_by_pk(rows, pk_flags, &old_row) {
822            if rows[idx] != old_row {
823                match handler(ConflictType::Data, change) {
824                    ConflictAction::OmitChange => return Ok(false),
825                    ConflictAction::Replace => {
826                        rows.remove(idx);
827                        return Ok(true);
828                    }
829                    ConflictAction::Abort => return Err(applied),
830                }
831            }
832            rows.remove(idx);
833            Ok(true)
834        } else {
835            match handler(ConflictType::NotFound, change) {
836                ConflictAction::OmitChange | ConflictAction::Replace => Ok(false),
837                ConflictAction::Abort => Err(applied),
838            }
839        }
840    }
841
842    fn apply_update<F>(
843        rows: &mut [Vec<SqliteValue>],
844        pk_flags: &[bool],
845        change: &ChangesetRow,
846        handler: &mut F,
847        applied: usize,
848    ) -> RowApplyResult
849    where
850        F: FnMut(ConflictType, &ChangesetRow) -> ConflictAction,
851    {
852        let old_row: Vec<SqliteValue> = change
853            .old_values
854            .iter()
855            .map(ChangesetValue::to_sqlite)
856            .collect();
857        if let Some(idx) = Self::find_row_by_pk(rows, pk_flags, &old_row) {
858            let old_match =
859                change
860                    .old_values
861                    .iter()
862                    .zip(rows[idx].iter())
863                    .all(|(cv, sv)| match cv {
864                        ChangesetValue::Undefined => true,
865                        _ => cv.to_sqlite() == *sv,
866                    });
867            if !old_match {
868                match handler(ConflictType::Data, change) {
869                    ConflictAction::OmitChange => return Ok(false),
870                    ConflictAction::Replace => {}
871                    ConflictAction::Abort => return Err(applied),
872                }
873            }
874            let row = &mut rows[idx];
875            for (i, nv) in change.new_values.iter().enumerate() {
876                if *nv != ChangesetValue::Undefined {
877                    if let Some(cell) = row.get_mut(i) {
878                        *cell = nv.to_sqlite();
879                    }
880                }
881            }
882            Ok(true)
883        } else {
884            match handler(ConflictType::NotFound, change) {
885                ConflictAction::OmitChange | ConflictAction::Replace => Ok(false),
886                ConflictAction::Abort => Err(applied),
887            }
888        }
889    }
890
891    fn find_row_by_pk(
892        rows: &[Vec<SqliteValue>],
893        pk_flags: &[bool],
894        target: &[SqliteValue],
895    ) -> Option<usize> {
896        rows.iter().position(|row| {
897            pk_flags
898                .iter()
899                .enumerate()
900                .filter(|&(_, &is_pk)| is_pk)
901                .all(|(i, _)| row.get(i).zip(target.get(i)).is_some_and(|(a, b)| a == b))
902        })
903    }
904}
905
906// ---------------------------------------------------------------------------
907// Varint helpers (re-exported for convenience)
908// ---------------------------------------------------------------------------
909
910/// Compute the byte length of a varint-encoded value.
911#[must_use]
912pub const fn changeset_varint_len(value: u64) -> usize {
913    varint_len(value)
914}
915
916// ---------------------------------------------------------------------------
917// Tests
918// ---------------------------------------------------------------------------
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923
924    #[test]
925    fn test_extension_name_matches_crate_suffix() {
926        let expected = env!("CARGO_PKG_NAME")
927            .strip_prefix("fsqlite-ext-")
928            .expect("extension crates should use fsqlite-ext-* naming");
929        assert_eq!(extension_name(), expected);
930    }
931
932    // -----------------------------------------------------------------------
933    // ChangeOp round-trip
934    // -----------------------------------------------------------------------
935
936    #[test]
937    fn test_change_op_byte_roundtrip() {
938        for op in [ChangeOp::Insert, ChangeOp::Delete, ChangeOp::Update] {
939            assert_eq!(ChangeOp::from_byte(op.as_byte()), Some(op));
940        }
941        assert_eq!(ChangeOp::from_byte(0xFF), None);
942    }
943
944    #[test]
945    fn test_change_op_byte_values() {
946        assert_eq!(ChangeOp::Insert.as_byte(), 18);
947        assert_eq!(ChangeOp::Delete.as_byte(), 9);
948        assert_eq!(ChangeOp::Update.as_byte(), 23);
949    }
950
951    // -----------------------------------------------------------------------
952    // ChangesetValue encoding / decoding
953    // -----------------------------------------------------------------------
954
955    #[test]
956    fn test_changeset_value_undefined() {
957        let mut buf = Vec::new();
958        ChangesetValue::Undefined.encode(&mut buf);
959        assert_eq!(buf, [VAL_UNDEFINED]);
960        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
961        assert_eq!(val, ChangesetValue::Undefined);
962        assert_eq!(consumed, 1);
963    }
964
965    #[test]
966    fn test_changeset_value_null() {
967        let mut buf = Vec::new();
968        ChangesetValue::Null.encode(&mut buf);
969        assert_eq!(buf, [VAL_NULL]);
970        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
971        assert_eq!(val, ChangesetValue::Null);
972        assert_eq!(consumed, 1);
973    }
974
975    #[test]
976    fn test_changeset_value_integer() {
977        let mut buf = Vec::new();
978        ChangesetValue::Integer(42).encode(&mut buf);
979        assert_eq!(buf[0], VAL_INTEGER);
980        assert_eq!(&buf[1..], 42_i64.to_be_bytes());
981        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
982        assert_eq!(val, ChangesetValue::Integer(42));
983        assert_eq!(consumed, 9);
984    }
985
986    #[test]
987    fn test_changeset_value_integer_negative() {
988        let mut buf = Vec::new();
989        ChangesetValue::Integer(-12_345).encode(&mut buf);
990        let (val, _) = ChangesetValue::decode(&buf, 0).unwrap();
991        assert_eq!(val, ChangesetValue::Integer(-12_345));
992    }
993
994    #[test]
995    fn test_changeset_value_real() {
996        let mut buf = Vec::new();
997        ChangesetValue::Real(1.23).encode(&mut buf);
998        assert_eq!(buf[0], VAL_REAL);
999        assert_eq!(&buf[1..], 1.23_f64.to_be_bytes());
1000        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
1001        assert_eq!(val, ChangesetValue::Real(1.23));
1002        assert_eq!(consumed, 9);
1003    }
1004
1005    #[test]
1006    fn test_changeset_value_text() {
1007        let mut buf = Vec::new();
1008        ChangesetValue::Text("hello".to_owned()).encode(&mut buf);
1009        assert_eq!(buf[0], VAL_TEXT);
1010        // varint(5) = 0x05, then b"hello"
1011        assert_eq!(buf[1], 5);
1012        assert_eq!(&buf[2..], b"hello");
1013        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
1014        assert_eq!(val, ChangesetValue::Text("hello".to_owned()));
1015        assert_eq!(consumed, 7); // 1 type + 1 varint + 5 data
1016    }
1017
1018    #[test]
1019    fn test_changeset_value_text_empty() {
1020        let mut buf = Vec::new();
1021        ChangesetValue::Text(String::new()).encode(&mut buf);
1022        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
1023        assert_eq!(val, ChangesetValue::Text(String::new()));
1024        assert_eq!(consumed, 2); // 1 type + 1 varint(0)
1025    }
1026
1027    #[test]
1028    fn test_changeset_value_blob() {
1029        let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
1030        let mut buf = Vec::new();
1031        ChangesetValue::Blob(data.clone()).encode(&mut buf);
1032        assert_eq!(buf[0], VAL_BLOB);
1033        assert_eq!(buf[1], 4); // varint(4)
1034        assert_eq!(&buf[2..], &data);
1035        let (val, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
1036        assert_eq!(val, ChangesetValue::Blob(data));
1037        assert_eq!(consumed, 6);
1038    }
1039
1040    #[test]
1041    fn test_changeset_value_decode_bad_type() {
1042        assert!(ChangesetValue::decode(&[0xFF], 0).is_none());
1043    }
1044
1045    #[test]
1046    fn test_changeset_value_decode_truncated() {
1047        // Integer needs 9 bytes total, give only 5.
1048        assert!(ChangesetValue::decode(&[VAL_INTEGER, 0, 0, 0, 0], 0).is_none());
1049    }
1050
1051    // -----------------------------------------------------------------------
1052    // TableInfo encoding / decoding
1053    // -----------------------------------------------------------------------
1054
1055    #[test]
1056    fn test_table_info_roundtrip() {
1057        let info = TableInfo {
1058            name: "users".to_owned(),
1059            column_count: 3,
1060            pk_flags: vec![true, false, false],
1061        };
1062        let mut buf = Vec::new();
1063        info.encode(&mut buf);
1064
1065        assert_eq!(buf[0], TABLE_HEADER_BYTE);
1066        let (decoded, consumed) = TableInfo::decode(&buf, 0).unwrap();
1067        assert_eq!(decoded, info);
1068        assert_eq!(consumed, buf.len());
1069    }
1070
1071    #[test]
1072    fn test_table_info_header_byte() {
1073        let info = TableInfo {
1074            name: "t".to_owned(),
1075            column_count: 1,
1076            pk_flags: vec![true],
1077        };
1078        let mut buf = Vec::new();
1079        info.encode(&mut buf);
1080        assert_eq!(buf[0], 0x54); // 'T'
1081    }
1082
1083    #[test]
1084    fn test_table_info_nul_terminated_name() {
1085        let info = TableInfo {
1086            name: "orders".to_owned(),
1087            column_count: 2,
1088            pk_flags: vec![true, false],
1089        };
1090        let mut buf = Vec::new();
1091        info.encode(&mut buf);
1092        // Last byte should be NUL terminator.
1093        assert_eq!(*buf.last().unwrap(), 0x00);
1094    }
1095
1096    // -----------------------------------------------------------------------
1097    // Session — basic tracking
1098    // -----------------------------------------------------------------------
1099
1100    #[test]
1101    fn test_session_create() {
1102        let session = Session::new();
1103        assert!(session.tables.is_empty());
1104        assert!(session.changes.is_empty());
1105    }
1106
1107    #[test]
1108    fn test_session_attach_table() {
1109        let mut session = Session::new();
1110        session.attach_table("users", 3, vec![true, false, false]);
1111        assert_eq!(session.tables.len(), 1);
1112        assert_eq!(session.tables[0].name, "users");
1113    }
1114
1115    #[test]
1116    fn test_session_record_insert() {
1117        let mut session = Session::new();
1118        session.attach_table("t", 2, vec![true, false]);
1119        session.record_insert(
1120            "t",
1121            vec![
1122                ChangesetValue::Integer(1),
1123                ChangesetValue::Text("a".to_owned()),
1124            ],
1125        );
1126        let cs = session.changeset();
1127        assert_eq!(cs.tables.len(), 1);
1128        assert_eq!(cs.tables[0].rows.len(), 1);
1129        assert_eq!(cs.tables[0].rows[0].op, ChangeOp::Insert);
1130    }
1131
1132    #[test]
1133    fn test_session_record_delete() {
1134        let mut session = Session::new();
1135        session.attach_table("t", 2, vec![true, false]);
1136        session.record_delete(
1137            "t",
1138            vec![
1139                ChangesetValue::Integer(1),
1140                ChangesetValue::Text("a".to_owned()),
1141            ],
1142        );
1143        let cs = session.changeset();
1144        assert_eq!(cs.tables[0].rows[0].op, ChangeOp::Delete);
1145    }
1146
1147    #[test]
1148    fn test_session_record_update() {
1149        let mut session = Session::new();
1150        session.attach_table("t", 2, vec![true, false]);
1151        session.record_update(
1152            "t",
1153            vec![
1154                ChangesetValue::Integer(1),
1155                ChangesetValue::Text("a".to_owned()),
1156            ],
1157            vec![
1158                ChangesetValue::Undefined,
1159                ChangesetValue::Text("b".to_owned()),
1160            ],
1161        );
1162        let cs = session.changeset();
1163        let row = &cs.tables[0].rows[0];
1164        assert_eq!(row.op, ChangeOp::Update);
1165        assert_eq!(row.old_values[1], ChangesetValue::Text("a".to_owned()));
1166        assert_eq!(row.new_values[0], ChangesetValue::Undefined);
1167        assert_eq!(row.new_values[1], ChangesetValue::Text("b".to_owned()));
1168    }
1169
1170    #[test]
1171    fn test_session_multiple_tables() {
1172        let mut session = Session::new();
1173        session.attach_table("a", 1, vec![true]);
1174        session.attach_table("b", 1, vec![true]);
1175        session.record_insert("a", vec![ChangesetValue::Integer(1)]);
1176        session.record_insert("b", vec![ChangesetValue::Integer(2)]);
1177        let cs = session.changeset();
1178        assert_eq!(cs.tables.len(), 2);
1179        assert_eq!(cs.tables[0].info.name, "a");
1180        assert_eq!(cs.tables[1].info.name, "b");
1181    }
1182
1183    #[test]
1184    fn test_session_pk_columns() {
1185        let mut session = Session::new();
1186        session.attach_table("t", 3, vec![true, false, true]);
1187        let cs = session.changeset();
1188        // Even with no changes, table metadata is not emitted (no rows).
1189        assert!(cs.tables.is_empty());
1190        // Add a change so the table shows up.
1191        session.record_insert(
1192            "t",
1193            vec![
1194                ChangesetValue::Integer(1),
1195                ChangesetValue::Text("x".to_owned()),
1196                ChangesetValue::Integer(2),
1197            ],
1198        );
1199        let cs = session.changeset();
1200        assert_eq!(cs.tables[0].info.pk_flags, vec![true, false, true]);
1201    }
1202
1203    // -----------------------------------------------------------------------
1204    // Changeset binary format
1205    // -----------------------------------------------------------------------
1206
1207    #[test]
1208    fn test_changeset_binary_format() {
1209        let mut session = Session::new();
1210        session.attach_table("t", 2, vec![true, false]);
1211        session.record_insert(
1212            "t",
1213            vec![
1214                ChangesetValue::Integer(1),
1215                ChangesetValue::Text("hi".to_owned()),
1216            ],
1217        );
1218        let encoded = session.changeset().encode();
1219        // Table header: 'T', varint(2), pk[1,0], "t\0"
1220        assert_eq!(encoded[0], 0x54);
1221        // Verify we can decode it back.
1222        let decoded = Changeset::decode(&encoded).unwrap();
1223        assert_eq!(decoded.tables.len(), 1);
1224        assert_eq!(decoded.tables[0].info.name, "t");
1225        assert_eq!(decoded.tables[0].rows[0].op, ChangeOp::Insert);
1226    }
1227
1228    #[test]
1229    fn test_changeset_roundtrip() {
1230        let mut session = Session::new();
1231        session.attach_table("users", 3, vec![true, false, false]);
1232        session.record_insert(
1233            "users",
1234            vec![
1235                ChangesetValue::Integer(1),
1236                ChangesetValue::Text("Alice".to_owned()),
1237                ChangesetValue::Integer(30),
1238            ],
1239        );
1240        session.record_insert(
1241            "users",
1242            vec![
1243                ChangesetValue::Integer(2),
1244                ChangesetValue::Text("Bob".to_owned()),
1245                ChangesetValue::Integer(25),
1246            ],
1247        );
1248        session.record_delete(
1249            "users",
1250            vec![
1251                ChangesetValue::Integer(1),
1252                ChangesetValue::Text("Alice".to_owned()),
1253                ChangesetValue::Integer(30),
1254            ],
1255        );
1256        session.record_update(
1257            "users",
1258            vec![
1259                ChangesetValue::Integer(2),
1260                ChangesetValue::Text("Bob".to_owned()),
1261                ChangesetValue::Integer(25),
1262            ],
1263            vec![
1264                ChangesetValue::Undefined,
1265                ChangesetValue::Text("Robert".to_owned()),
1266                ChangesetValue::Undefined,
1267            ],
1268        );
1269
1270        let cs = session.changeset();
1271        let encoded = cs.encode();
1272        let decoded = Changeset::decode(&encoded).unwrap();
1273        assert_eq!(decoded, cs);
1274    }
1275
1276    // -----------------------------------------------------------------------
1277    // Changeset inversion
1278    // -----------------------------------------------------------------------
1279
1280    #[test]
1281    fn test_changeset_invert_insert() {
1282        let row = ChangesetRow {
1283            op: ChangeOp::Insert,
1284            old_values: Vec::new(),
1285            new_values: vec![ChangesetValue::Integer(1)],
1286        };
1287        let inv = row.invert();
1288        assert_eq!(inv.op, ChangeOp::Delete);
1289        assert_eq!(inv.old_values, vec![ChangesetValue::Integer(1)]);
1290        assert!(inv.new_values.is_empty());
1291    }
1292
1293    #[test]
1294    fn test_changeset_invert_delete() {
1295        let row = ChangesetRow {
1296            op: ChangeOp::Delete,
1297            old_values: vec![ChangesetValue::Integer(1)],
1298            new_values: Vec::new(),
1299        };
1300        let inv = row.invert();
1301        assert_eq!(inv.op, ChangeOp::Insert);
1302        assert!(inv.old_values.is_empty());
1303        assert_eq!(inv.new_values, vec![ChangesetValue::Integer(1)]);
1304    }
1305
1306    #[test]
1307    fn test_changeset_invert_update() {
1308        let row = ChangesetRow {
1309            op: ChangeOp::Update,
1310            old_values: vec![
1311                ChangesetValue::Integer(1),
1312                ChangesetValue::Text("old".to_owned()),
1313            ],
1314            new_values: vec![
1315                ChangesetValue::Undefined,
1316                ChangesetValue::Text("new".to_owned()),
1317            ],
1318        };
1319        let inv = row.invert();
1320        assert_eq!(inv.op, ChangeOp::Update);
1321        assert_eq!(inv.old_values[0], ChangesetValue::Undefined);
1322        assert_eq!(inv.old_values[1], ChangesetValue::Text("new".to_owned()));
1323        assert_eq!(inv.new_values[0], ChangesetValue::Integer(1));
1324        assert_eq!(inv.new_values[1], ChangesetValue::Text("old".to_owned()));
1325    }
1326
1327    // -----------------------------------------------------------------------
1328    // Changeset concat
1329    // -----------------------------------------------------------------------
1330
1331    #[test]
1332    fn test_changeset_concat() {
1333        let mut cs1 = Changeset::new();
1334        cs1.tables.push(TableChangeset {
1335            info: TableInfo {
1336                name: "a".to_owned(),
1337                column_count: 1,
1338                pk_flags: vec![true],
1339            },
1340            rows: vec![ChangesetRow {
1341                op: ChangeOp::Insert,
1342                old_values: Vec::new(),
1343                new_values: vec![ChangesetValue::Integer(1)],
1344            }],
1345        });
1346        let cs2 = Changeset {
1347            tables: vec![TableChangeset {
1348                info: TableInfo {
1349                    name: "b".to_owned(),
1350                    column_count: 1,
1351                    pk_flags: vec![true],
1352                },
1353                rows: vec![ChangesetRow {
1354                    op: ChangeOp::Insert,
1355                    old_values: Vec::new(),
1356                    new_values: vec![ChangesetValue::Integer(2)],
1357                }],
1358            }],
1359        };
1360        cs1.concat(&cs2);
1361        assert_eq!(cs1.tables.len(), 2);
1362    }
1363
1364    // -----------------------------------------------------------------------
1365    // Patchset format
1366    // -----------------------------------------------------------------------
1367
1368    #[test]
1369    fn test_patchset_format_omits_old_values() {
1370        let mut session = Session::new();
1371        session.attach_table("t", 3, vec![true, false, false]);
1372        session.record_update(
1373            "t",
1374            vec![
1375                ChangesetValue::Integer(1),
1376                ChangesetValue::Text("old_name".to_owned()),
1377                ChangesetValue::Integer(100),
1378            ],
1379            vec![
1380                ChangesetValue::Undefined,
1381                ChangesetValue::Text("new_name".to_owned()),
1382                ChangesetValue::Undefined,
1383            ],
1384        );
1385        let changeset_bytes = session.changeset().encode();
1386        let patchset_bytes = session.patchset();
1387        // Patchset should be smaller (omits non-PK old values).
1388        assert!(
1389            patchset_bytes.len() < changeset_bytes.len(),
1390            "patchset ({}) should be smaller than changeset ({})",
1391            patchset_bytes.len(),
1392            changeset_bytes.len(),
1393        );
1394    }
1395
1396    #[test]
1397    fn test_patchset_insert_same_as_changeset() {
1398        let mut session = Session::new();
1399        session.attach_table("t", 2, vec![true, false]);
1400        session.record_insert(
1401            "t",
1402            vec![
1403                ChangesetValue::Integer(1),
1404                ChangesetValue::Text("a".to_owned()),
1405            ],
1406        );
1407        let changeset_bytes = session.changeset().encode();
1408        let patchset_bytes = session.patchset();
1409        // For INSERT, patchset and changeset are identical.
1410        assert_eq!(changeset_bytes, patchset_bytes);
1411    }
1412
1413    // -----------------------------------------------------------------------
1414    // Apply — successful cases
1415    // -----------------------------------------------------------------------
1416
1417    #[test]
1418    fn test_apply_insert() {
1419        let cs = Changeset {
1420            tables: vec![TableChangeset {
1421                info: TableInfo {
1422                    name: "t".to_owned(),
1423                    column_count: 2,
1424                    pk_flags: vec![true, false],
1425                },
1426                rows: vec![ChangesetRow {
1427                    op: ChangeOp::Insert,
1428                    old_values: Vec::new(),
1429                    new_values: vec![
1430                        ChangesetValue::Integer(1),
1431                        ChangesetValue::Text("hello".to_owned()),
1432                    ],
1433                }],
1434            }],
1435        };
1436
1437        let mut target = SimpleTarget::default();
1438        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
1439        assert_eq!(
1440            outcome,
1441            ApplyOutcome::Success {
1442                applied: 1,
1443                skipped: 0
1444            }
1445        );
1446        assert_eq!(
1447            target.tables["t"],
1448            vec![vec![
1449                SqliteValue::Integer(1),
1450                SqliteValue::Text("hello".to_owned())
1451            ]]
1452        );
1453    }
1454
1455    #[test]
1456    fn test_apply_delete() {
1457        let mut target = SimpleTarget::default();
1458        target.tables.insert(
1459            "t".to_owned(),
1460            vec![vec![
1461                SqliteValue::Integer(1),
1462                SqliteValue::Text("hello".to_owned()),
1463            ]],
1464        );
1465
1466        let cs = Changeset {
1467            tables: vec![TableChangeset {
1468                info: TableInfo {
1469                    name: "t".to_owned(),
1470                    column_count: 2,
1471                    pk_flags: vec![true, false],
1472                },
1473                rows: vec![ChangesetRow {
1474                    op: ChangeOp::Delete,
1475                    old_values: vec![
1476                        ChangesetValue::Integer(1),
1477                        ChangesetValue::Text("hello".to_owned()),
1478                    ],
1479                    new_values: Vec::new(),
1480                }],
1481            }],
1482        };
1483
1484        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
1485        assert_eq!(
1486            outcome,
1487            ApplyOutcome::Success {
1488                applied: 1,
1489                skipped: 0
1490            }
1491        );
1492        assert!(target.tables["t"].is_empty());
1493    }
1494
1495    #[test]
1496    fn test_apply_update() {
1497        let mut target = SimpleTarget::default();
1498        target.tables.insert(
1499            "t".to_owned(),
1500            vec![vec![
1501                SqliteValue::Integer(1),
1502                SqliteValue::Text("old".to_owned()),
1503            ]],
1504        );
1505
1506        let cs = Changeset {
1507            tables: vec![TableChangeset {
1508                info: TableInfo {
1509                    name: "t".to_owned(),
1510                    column_count: 2,
1511                    pk_flags: vec![true, false],
1512                },
1513                rows: vec![ChangesetRow {
1514                    op: ChangeOp::Update,
1515                    old_values: vec![
1516                        ChangesetValue::Integer(1),
1517                        ChangesetValue::Text("old".to_owned()),
1518                    ],
1519                    new_values: vec![
1520                        ChangesetValue::Undefined,
1521                        ChangesetValue::Text("new".to_owned()),
1522                    ],
1523                }],
1524            }],
1525        };
1526
1527        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
1528        assert_eq!(
1529            outcome,
1530            ApplyOutcome::Success {
1531                applied: 1,
1532                skipped: 0
1533            }
1534        );
1535        assert_eq!(
1536            target.tables["t"][0],
1537            vec![SqliteValue::Integer(1), SqliteValue::Text("new".to_owned())]
1538        );
1539    }
1540
1541    // -----------------------------------------------------------------------
1542    // Apply — conflict scenarios
1543    // -----------------------------------------------------------------------
1544
1545    #[test]
1546    fn test_conflict_not_found() {
1547        let cs = Changeset {
1548            tables: vec![TableChangeset {
1549                info: TableInfo {
1550                    name: "t".to_owned(),
1551                    column_count: 1,
1552                    pk_flags: vec![true],
1553                },
1554                rows: vec![ChangesetRow {
1555                    op: ChangeOp::Delete,
1556                    old_values: vec![ChangesetValue::Integer(999)],
1557                    new_values: Vec::new(),
1558                }],
1559            }],
1560        };
1561        let mut target = SimpleTarget::default();
1562        let mut conflict_seen = None;
1563        let outcome = target.apply(&cs, |ct, _| {
1564            conflict_seen = Some(ct);
1565            ConflictAction::OmitChange
1566        });
1567        assert_eq!(conflict_seen, Some(ConflictType::NotFound));
1568        assert_eq!(
1569            outcome,
1570            ApplyOutcome::Success {
1571                applied: 0,
1572                skipped: 1
1573            }
1574        );
1575    }
1576
1577    #[test]
1578    fn test_conflict_data() {
1579        let mut target = SimpleTarget::default();
1580        target.tables.insert(
1581            "t".to_owned(),
1582            vec![vec![
1583                SqliteValue::Integer(1),
1584                SqliteValue::Text("actual".to_owned()),
1585            ]],
1586        );
1587
1588        let cs = Changeset {
1589            tables: vec![TableChangeset {
1590                info: TableInfo {
1591                    name: "t".to_owned(),
1592                    column_count: 2,
1593                    pk_flags: vec![true, false],
1594                },
1595                rows: vec![ChangesetRow {
1596                    op: ChangeOp::Delete,
1597                    old_values: vec![
1598                        ChangesetValue::Integer(1),
1599                        ChangesetValue::Text("expected".to_owned()),
1600                    ],
1601                    new_values: Vec::new(),
1602                }],
1603            }],
1604        };
1605
1606        let mut conflict_seen = None;
1607        let outcome = target.apply(&cs, |ct, _| {
1608            conflict_seen = Some(ct);
1609            ConflictAction::OmitChange
1610        });
1611        assert_eq!(conflict_seen, Some(ConflictType::Data));
1612        assert_eq!(
1613            outcome,
1614            ApplyOutcome::Success {
1615                applied: 0,
1616                skipped: 1
1617            }
1618        );
1619    }
1620
1621    #[test]
1622    fn test_conflict_unique_insert() {
1623        let mut target = SimpleTarget::default();
1624        target
1625            .tables
1626            .insert("t".to_owned(), vec![vec![SqliteValue::Integer(1)]]);
1627
1628        let cs = Changeset {
1629            tables: vec![TableChangeset {
1630                info: TableInfo {
1631                    name: "t".to_owned(),
1632                    column_count: 1,
1633                    pk_flags: vec![true],
1634                },
1635                rows: vec![ChangesetRow {
1636                    op: ChangeOp::Insert,
1637                    old_values: Vec::new(),
1638                    new_values: vec![ChangesetValue::Integer(1)], // Duplicate PK
1639                }],
1640            }],
1641        };
1642
1643        let mut conflict_seen = None;
1644        let outcome = target.apply(&cs, |ct, _| {
1645            conflict_seen = Some(ct);
1646            ConflictAction::OmitChange
1647        });
1648        assert_eq!(conflict_seen, Some(ConflictType::Conflict));
1649        assert_eq!(
1650            outcome,
1651            ApplyOutcome::Success {
1652                applied: 0,
1653                skipped: 1
1654            }
1655        );
1656    }
1657
1658    #[test]
1659    fn test_conflict_omit_skips() {
1660        let mut target = SimpleTarget::default();
1661        let cs = Changeset {
1662            tables: vec![TableChangeset {
1663                info: TableInfo {
1664                    name: "t".to_owned(),
1665                    column_count: 1,
1666                    pk_flags: vec![true],
1667                },
1668                rows: vec![ChangesetRow {
1669                    op: ChangeOp::Delete,
1670                    old_values: vec![ChangesetValue::Integer(1)],
1671                    new_values: Vec::new(),
1672                }],
1673            }],
1674        };
1675        let outcome = target.apply(&cs, |_, _| ConflictAction::OmitChange);
1676        assert_eq!(
1677            outcome,
1678            ApplyOutcome::Success {
1679                applied: 0,
1680                skipped: 1
1681            }
1682        );
1683    }
1684
1685    #[test]
1686    fn test_conflict_replace_insert() {
1687        let mut target = SimpleTarget::default();
1688        target.tables.insert(
1689            "t".to_owned(),
1690            vec![vec![
1691                SqliteValue::Integer(1),
1692                SqliteValue::Text("old".to_owned()),
1693            ]],
1694        );
1695
1696        let cs = Changeset {
1697            tables: vec![TableChangeset {
1698                info: TableInfo {
1699                    name: "t".to_owned(),
1700                    column_count: 2,
1701                    pk_flags: vec![true, false],
1702                },
1703                rows: vec![ChangesetRow {
1704                    op: ChangeOp::Insert,
1705                    old_values: Vec::new(),
1706                    new_values: vec![
1707                        ChangesetValue::Integer(1),
1708                        ChangesetValue::Text("replaced".to_owned()),
1709                    ],
1710                }],
1711            }],
1712        };
1713
1714        let outcome = target.apply(&cs, |_, _| ConflictAction::Replace);
1715        assert_eq!(
1716            outcome,
1717            ApplyOutcome::Success {
1718                applied: 1,
1719                skipped: 0
1720            }
1721        );
1722        assert_eq!(
1723            target.tables["t"][0],
1724            vec![
1725                SqliteValue::Integer(1),
1726                SqliteValue::Text("replaced".to_owned())
1727            ]
1728        );
1729    }
1730
1731    #[test]
1732    fn test_conflict_abort_stops_apply() {
1733        let mut target = SimpleTarget::default();
1734        let cs = Changeset {
1735            tables: vec![TableChangeset {
1736                info: TableInfo {
1737                    name: "t".to_owned(),
1738                    column_count: 1,
1739                    pk_flags: vec![true],
1740                },
1741                rows: vec![
1742                    ChangesetRow {
1743                        op: ChangeOp::Delete,
1744                        old_values: vec![ChangesetValue::Integer(1)],
1745                        new_values: Vec::new(),
1746                    },
1747                    ChangesetRow {
1748                        op: ChangeOp::Insert,
1749                        old_values: Vec::new(),
1750                        new_values: vec![ChangesetValue::Integer(2)],
1751                    },
1752                ],
1753            }],
1754        };
1755        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
1756        assert_eq!(outcome, ApplyOutcome::Aborted { applied: 0 });
1757        // Second row should NOT have been applied.
1758        assert!(!target.tables.contains_key("t") || target.tables["t"].is_empty());
1759    }
1760
1761    // -----------------------------------------------------------------------
1762    // Full round-trip: session → changeset → apply → verify
1763    // -----------------------------------------------------------------------
1764
1765    #[test]
1766    fn test_changeset_full_roundtrip() {
1767        // Build changeset via session.
1768        let mut session = Session::new();
1769        session.attach_table("users", 3, vec![true, false, false]);
1770        session.record_insert(
1771            "users",
1772            vec![
1773                ChangesetValue::Integer(1),
1774                ChangesetValue::Text("Alice".to_owned()),
1775                ChangesetValue::Integer(30),
1776            ],
1777        );
1778        session.record_insert(
1779            "users",
1780            vec![
1781                ChangesetValue::Integer(2),
1782                ChangesetValue::Text("Bob".to_owned()),
1783                ChangesetValue::Integer(25),
1784            ],
1785        );
1786
1787        let cs = session.changeset();
1788
1789        // Apply to empty target.
1790        let mut target = SimpleTarget::default();
1791        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
1792        assert_eq!(
1793            outcome,
1794            ApplyOutcome::Success {
1795                applied: 2,
1796                skipped: 0
1797            }
1798        );
1799        assert_eq!(target.tables["users"].len(), 2);
1800        assert_eq!(
1801            target.tables["users"][0][1],
1802            SqliteValue::Text("Alice".to_owned())
1803        );
1804        assert_eq!(
1805            target.tables["users"][1][1],
1806            SqliteValue::Text("Bob".to_owned())
1807        );
1808    }
1809
1810    #[test]
1811    fn test_changeset_invert_undoes_changes() {
1812        let mut session = Session::new();
1813        session.attach_table("t", 2, vec![true, false]);
1814        session.record_insert(
1815            "t",
1816            vec![
1817                ChangesetValue::Integer(1),
1818                ChangesetValue::Text("a".to_owned()),
1819            ],
1820        );
1821
1822        let cs = session.changeset();
1823        let inv = cs.invert();
1824
1825        // Apply original changeset.
1826        let mut target = SimpleTarget::default();
1827        target.apply(&cs, |_, _| ConflictAction::Abort);
1828        assert_eq!(target.tables["t"].len(), 1);
1829
1830        // Apply inverted changeset — should remove the row.
1831        target.apply(&inv, |_, _| ConflictAction::Abort);
1832        assert!(target.tables["t"].is_empty());
1833    }
1834
1835    // -----------------------------------------------------------------------
1836    // ChangesetValue <-> SqliteValue conversion
1837    // -----------------------------------------------------------------------
1838
1839    #[test]
1840    fn test_changeset_value_from_sqlite() {
1841        assert_eq!(
1842            ChangesetValue::from_sqlite(&SqliteValue::Null),
1843            ChangesetValue::Null
1844        );
1845        assert_eq!(
1846            ChangesetValue::from_sqlite(&SqliteValue::Integer(42)),
1847            ChangesetValue::Integer(42)
1848        );
1849        assert_eq!(
1850            ChangesetValue::from_sqlite(&SqliteValue::Float(1.5)),
1851            ChangesetValue::Real(1.5)
1852        );
1853        assert_eq!(
1854            ChangesetValue::from_sqlite(&SqliteValue::Text("x".to_owned())),
1855            ChangesetValue::Text("x".to_owned())
1856        );
1857        assert_eq!(
1858            ChangesetValue::from_sqlite(&SqliteValue::Blob(vec![1, 2])),
1859            ChangesetValue::Blob(vec![1, 2])
1860        );
1861    }
1862
1863    #[test]
1864    fn test_changeset_value_to_sqlite() {
1865        assert_eq!(ChangesetValue::Undefined.to_sqlite(), SqliteValue::Null);
1866        assert_eq!(ChangesetValue::Null.to_sqlite(), SqliteValue::Null);
1867        assert_eq!(
1868            ChangesetValue::Integer(7).to_sqlite(),
1869            SqliteValue::Integer(7)
1870        );
1871        assert_eq!(
1872            ChangesetValue::Real(2.5).to_sqlite(),
1873            SqliteValue::Float(2.5)
1874        );
1875        assert_eq!(
1876            ChangesetValue::Text("hi".to_owned()).to_sqlite(),
1877            SqliteValue::Text("hi".to_owned())
1878        );
1879        assert_eq!(
1880            ChangesetValue::Blob(vec![0xAB]).to_sqlite(),
1881            SqliteValue::Blob(vec![0xAB])
1882        );
1883    }
1884
1885    // -----------------------------------------------------------------------
1886    // ChangeOp edge cases
1887    // -----------------------------------------------------------------------
1888
1889    #[test]
1890    fn test_change_op_from_byte_exhaustive_invalid() {
1891        for b in 0..=255u8 {
1892            if matches!(b, 0x12 | 0x09 | 0x17) {
1893                assert!(ChangeOp::from_byte(b).is_some());
1894            } else {
1895                assert!(
1896                    ChangeOp::from_byte(b).is_none(),
1897                    "byte {b:#x} should be None"
1898                );
1899            }
1900        }
1901    }
1902
1903    #[test]
1904    fn test_change_op_copy_clone_eq() {
1905        let a = ChangeOp::Insert;
1906        let b = a;
1907        assert_eq!(a, b);
1908        assert_ne!(ChangeOp::Insert, ChangeOp::Delete);
1909        assert_ne!(ChangeOp::Delete, ChangeOp::Update);
1910    }
1911
1912    #[test]
1913    fn test_change_op_debug() {
1914        let s = format!("{:?}", ChangeOp::Insert);
1915        assert_eq!(s, "Insert");
1916    }
1917
1918    // -----------------------------------------------------------------------
1919    // ChangesetValue edge cases
1920    // -----------------------------------------------------------------------
1921
1922    #[test]
1923    fn test_changeset_value_integer_boundaries() {
1924        for &val in &[i64::MIN, i64::MAX, 0, -1, 1] {
1925            let mut buf = Vec::new();
1926            ChangesetValue::Integer(val).encode(&mut buf);
1927            let (decoded, _) = ChangesetValue::decode(&buf, 0).unwrap();
1928            assert_eq!(decoded, ChangesetValue::Integer(val));
1929        }
1930    }
1931
1932    #[test]
1933    fn test_changeset_value_real_special() {
1934        for &val in &[
1935            0.0,
1936            -0.0,
1937            f64::MAX,
1938            f64::MIN,
1939            f64::MIN_POSITIVE,
1940            f64::EPSILON,
1941        ] {
1942            let mut buf = Vec::new();
1943            ChangesetValue::Real(val).encode(&mut buf);
1944            let (decoded, _) = ChangesetValue::decode(&buf, 0).unwrap();
1945            assert_eq!(decoded, ChangesetValue::Real(val));
1946        }
1947    }
1948
1949    #[test]
1950    fn test_changeset_value_real_nan_roundtrip() {
1951        let mut buf = Vec::new();
1952        ChangesetValue::Real(f64::NAN).encode(&mut buf);
1953        let (decoded, _) = ChangesetValue::decode(&buf, 0).unwrap();
1954        if let ChangesetValue::Real(f) = decoded {
1955            assert!(f.is_nan());
1956        } else {
1957            panic!("expected Real");
1958        }
1959    }
1960
1961    #[test]
1962    fn test_changeset_value_blob_empty() {
1963        let mut buf = Vec::new();
1964        ChangesetValue::Blob(Vec::new()).encode(&mut buf);
1965        let (decoded, consumed) = ChangesetValue::decode(&buf, 0).unwrap();
1966        assert_eq!(decoded, ChangesetValue::Blob(Vec::new()));
1967        assert_eq!(consumed, 2); // type + varint(0)
1968    }
1969
1970    #[test]
1971    fn test_changeset_value_text_unicode() {
1972        let text = "\u{1F600}\u{1F4A9}\u{2603}"; // emoji + snowman
1973        let mut buf = Vec::new();
1974        ChangesetValue::Text(text.to_owned()).encode(&mut buf);
1975        let (decoded, _) = ChangesetValue::decode(&buf, 0).unwrap();
1976        assert_eq!(decoded, ChangesetValue::Text(text.to_owned()));
1977    }
1978
1979    #[test]
1980    fn test_changeset_value_decode_at_offset() {
1981        let mut buf = Vec::new();
1982        ChangesetValue::Null.encode(&mut buf); // 1 byte
1983        ChangesetValue::Integer(42).encode(&mut buf); // 9 bytes
1984        let (val, consumed) = ChangesetValue::decode(&buf, 1).unwrap();
1985        assert_eq!(val, ChangesetValue::Integer(42));
1986        assert_eq!(consumed, 9);
1987    }
1988
1989    #[test]
1990    fn test_changeset_value_decode_empty_slice() {
1991        assert!(ChangesetValue::decode(&[], 0).is_none());
1992    }
1993
1994    #[test]
1995    fn test_changeset_value_decode_offset_beyond_len() {
1996        assert!(ChangesetValue::decode(&[VAL_NULL], 5).is_none());
1997    }
1998
1999    #[test]
2000    fn test_changeset_value_decode_truncated_real() {
2001        assert!(ChangesetValue::decode(&[VAL_REAL, 0, 0, 0], 0).is_none());
2002    }
2003
2004    #[test]
2005    fn test_changeset_value_decode_truncated_text() {
2006        // Type byte + varint(10) but only 3 content bytes
2007        let mut buf = vec![VAL_TEXT, 10, b'a', b'b', b'c'];
2008        assert!(ChangesetValue::decode(&buf, 0).is_none());
2009        // Fix: provide exactly 10 bytes
2010        buf.extend_from_slice(&[0; 7]);
2011        // Non-UTF8 bytes should fail
2012        buf[5] = 0xFF;
2013        assert!(ChangesetValue::decode(&buf, 0).is_none());
2014    }
2015
2016    #[test]
2017    fn test_changeset_value_decode_truncated_blob() {
2018        let buf = vec![VAL_BLOB, 5, 1, 2]; // says 5 bytes, only has 2
2019        assert!(ChangesetValue::decode(&buf, 0).is_none());
2020    }
2021
2022    // -----------------------------------------------------------------------
2023    // ChangesetValue <-> SqliteValue round-trip
2024    // -----------------------------------------------------------------------
2025
2026    #[test]
2027    #[allow(clippy::approx_constant)]
2028    fn test_changeset_value_sqlite_roundtrip_all_types() {
2029        let values = vec![
2030            SqliteValue::Null,
2031            SqliteValue::Integer(0),
2032            SqliteValue::Integer(i64::MAX),
2033            SqliteValue::Float(3.14),
2034            SqliteValue::Text(String::new()),
2035            SqliteValue::Text("test".to_owned()),
2036            SqliteValue::Blob(vec![]),
2037            SqliteValue::Blob(vec![1, 2, 3]),
2038        ];
2039        for sv in &values {
2040            let cv = ChangesetValue::from_sqlite(sv);
2041            let back = cv.to_sqlite();
2042            assert_eq!(&back, sv);
2043        }
2044    }
2045
2046    // -----------------------------------------------------------------------
2047    // TableInfo edge cases
2048    // -----------------------------------------------------------------------
2049
2050    #[test]
2051    fn test_table_info_single_column() {
2052        let info = TableInfo {
2053            name: "x".to_owned(),
2054            column_count: 1,
2055            pk_flags: vec![true],
2056        };
2057        let mut buf = Vec::new();
2058        info.encode(&mut buf);
2059        let (decoded, consumed) = TableInfo::decode(&buf, 0).unwrap();
2060        assert_eq!(decoded, info);
2061        assert_eq!(consumed, buf.len());
2062    }
2063
2064    #[test]
2065    fn test_table_info_no_pk_columns() {
2066        let info = TableInfo {
2067            name: "t".to_owned(),
2068            column_count: 3,
2069            pk_flags: vec![false, false, false],
2070        };
2071        let mut buf = Vec::new();
2072        info.encode(&mut buf);
2073        let (decoded, _) = TableInfo::decode(&buf, 0).unwrap();
2074        assert_eq!(decoded.pk_flags, vec![false, false, false]);
2075    }
2076
2077    #[test]
2078    fn test_table_info_unicode_name() {
2079        let info = TableInfo {
2080            name: "\u{00FC}berschrift".to_owned(),
2081            column_count: 1,
2082            pk_flags: vec![true],
2083        };
2084        let mut buf = Vec::new();
2085        info.encode(&mut buf);
2086        let (decoded, _) = TableInfo::decode(&buf, 0).unwrap();
2087        assert_eq!(decoded.name, "\u{00FC}berschrift");
2088    }
2089
2090    #[test]
2091    fn test_table_info_decode_wrong_header() {
2092        assert!(TableInfo::decode(&[0x00, 0x01, 0x01, b't', 0x00], 0).is_none());
2093    }
2094
2095    #[test]
2096    fn test_table_info_decode_truncated() {
2097        assert!(TableInfo::decode(&[TABLE_HEADER_BYTE], 0).is_none());
2098        assert!(TableInfo::decode(&[TABLE_HEADER_BYTE, 3, 1], 0).is_none());
2099    }
2100
2101    #[test]
2102    fn test_table_info_decode_at_offset() {
2103        let mut buf = vec![0xFF, 0xFF]; // padding
2104        let info = TableInfo {
2105            name: "t".to_owned(),
2106            column_count: 1,
2107            pk_flags: vec![true],
2108        };
2109        info.encode(&mut buf);
2110        let (decoded, _) = TableInfo::decode(&buf, 2).unwrap();
2111        assert_eq!(decoded, info);
2112    }
2113
2114    // -----------------------------------------------------------------------
2115    // ChangesetRow edge cases
2116    // -----------------------------------------------------------------------
2117
2118    #[test]
2119    fn test_changeset_row_invert_double_is_identity() {
2120        let row = ChangesetRow {
2121            op: ChangeOp::Update,
2122            old_values: vec![
2123                ChangesetValue::Integer(1),
2124                ChangesetValue::Text("old".to_owned()),
2125            ],
2126            new_values: vec![
2127                ChangesetValue::Undefined,
2128                ChangesetValue::Text("new".to_owned()),
2129            ],
2130        };
2131        let double_inverted = row.invert().invert();
2132        assert_eq!(double_inverted, row);
2133    }
2134
2135    #[test]
2136    fn test_changeset_row_encode_decode_all_ops() {
2137        let col_count = 2;
2138        for op in [ChangeOp::Insert, ChangeOp::Delete, ChangeOp::Update] {
2139            let row = match op {
2140                ChangeOp::Insert => ChangesetRow {
2141                    op,
2142                    old_values: Vec::new(),
2143                    new_values: vec![ChangesetValue::Integer(1), ChangesetValue::Null],
2144                },
2145                ChangeOp::Delete => ChangesetRow {
2146                    op,
2147                    old_values: vec![ChangesetValue::Integer(1), ChangesetValue::Null],
2148                    new_values: Vec::new(),
2149                },
2150                ChangeOp::Update => ChangesetRow {
2151                    op,
2152                    old_values: vec![
2153                        ChangesetValue::Integer(1),
2154                        ChangesetValue::Text("a".to_owned()),
2155                    ],
2156                    new_values: vec![
2157                        ChangesetValue::Undefined,
2158                        ChangesetValue::Text("b".to_owned()),
2159                    ],
2160                },
2161            };
2162            let mut buf = Vec::new();
2163            row.encode_changeset(&mut buf);
2164            let (decoded, consumed) = ChangesetRow::decode_changeset(&buf, 0, col_count).unwrap();
2165            assert_eq!(decoded, row);
2166            assert_eq!(consumed, buf.len());
2167        }
2168    }
2169
2170    #[test]
2171    fn test_changeset_row_decode_bad_op() {
2172        assert!(ChangesetRow::decode_changeset(&[0xFF, VAL_NULL], 0, 1).is_none());
2173    }
2174
2175    // -----------------------------------------------------------------------
2176    // Patchset UPDATE: PK-only old values
2177    // -----------------------------------------------------------------------
2178
2179    #[test]
2180    fn test_patchset_update_only_pk_old() {
2181        let pk_flags = vec![true, false, false];
2182        let row = ChangesetRow {
2183            op: ChangeOp::Update,
2184            old_values: vec![
2185                ChangesetValue::Integer(1),
2186                ChangesetValue::Text("old_name".to_owned()),
2187                ChangesetValue::Integer(100),
2188            ],
2189            new_values: vec![
2190                ChangesetValue::Undefined,
2191                ChangesetValue::Text("new_name".to_owned()),
2192                ChangesetValue::Undefined,
2193            ],
2194        };
2195        let mut cs_buf = Vec::new();
2196        row.encode_changeset(&mut cs_buf);
2197        let mut ps_buf = Vec::new();
2198        row.encode_patchset(&mut ps_buf, &pk_flags);
2199        assert!(ps_buf.len() < cs_buf.len());
2200    }
2201
2202    #[test]
2203    fn test_patchset_delete_same_as_changeset() {
2204        let pk_flags = vec![true, false];
2205        let row = ChangesetRow {
2206            op: ChangeOp::Delete,
2207            old_values: vec![
2208                ChangesetValue::Integer(1),
2209                ChangesetValue::Text("a".to_owned()),
2210            ],
2211            new_values: Vec::new(),
2212        };
2213        let mut cs_buf = Vec::new();
2214        row.encode_changeset(&mut cs_buf);
2215        let mut ps_buf = Vec::new();
2216        row.encode_patchset(&mut ps_buf, &pk_flags);
2217        assert_eq!(cs_buf, ps_buf);
2218    }
2219
2220    // -----------------------------------------------------------------------
2221    // Session: unattached table
2222    // -----------------------------------------------------------------------
2223
2224    #[test]
2225    fn test_session_unattached_table_inferred() {
2226        let mut session = Session::new();
2227        // Record changes without attaching the table first
2228        session.record_insert("auto", vec![ChangesetValue::Integer(1)]);
2229        let cs = session.changeset();
2230        assert_eq!(cs.tables.len(), 1);
2231        assert_eq!(cs.tables[0].info.name, "auto");
2232        assert_eq!(cs.tables[0].info.column_count, 1);
2233        assert_eq!(cs.tables[0].info.pk_flags, vec![false]); // all non-PK
2234    }
2235
2236    #[test]
2237    fn test_session_empty_changeset() {
2238        let session = Session::new();
2239        let cs = session.changeset();
2240        assert!(cs.tables.is_empty());
2241        assert!(cs.encode().is_empty());
2242    }
2243
2244    #[test]
2245    fn test_session_empty_patchset() {
2246        let session = Session::new();
2247        assert!(session.patchset().is_empty());
2248    }
2249
2250    #[test]
2251    fn test_session_default_trait() {
2252        let session = Session::default();
2253        assert!(session.tables.is_empty());
2254    }
2255
2256    // -----------------------------------------------------------------------
2257    // Changeset edge cases
2258    // -----------------------------------------------------------------------
2259
2260    #[test]
2261    fn test_changeset_default_trait() {
2262        let cs = Changeset::default();
2263        assert!(cs.tables.is_empty());
2264    }
2265
2266    #[test]
2267    fn test_changeset_empty_encode_decode() {
2268        let cs = Changeset::new();
2269        let encoded = cs.encode();
2270        assert!(encoded.is_empty());
2271        let decoded = Changeset::decode(&encoded).unwrap();
2272        assert!(decoded.tables.is_empty());
2273    }
2274
2275    #[test]
2276    fn test_changeset_invert_is_self_inverse() {
2277        let mut session = Session::new();
2278        session.attach_table("t", 2, vec![true, false]);
2279        session.record_insert(
2280            "t",
2281            vec![
2282                ChangesetValue::Integer(1),
2283                ChangesetValue::Text("a".to_owned()),
2284            ],
2285        );
2286        session.record_delete(
2287            "t",
2288            vec![
2289                ChangesetValue::Integer(2),
2290                ChangesetValue::Text("b".to_owned()),
2291            ],
2292        );
2293        session.record_update(
2294            "t",
2295            vec![
2296                ChangesetValue::Integer(3),
2297                ChangesetValue::Text("c".to_owned()),
2298            ],
2299            vec![
2300                ChangesetValue::Undefined,
2301                ChangesetValue::Text("d".to_owned()),
2302            ],
2303        );
2304
2305        let cs = session.changeset();
2306        let double_inv = cs.invert().invert();
2307        assert_eq!(double_inv, cs);
2308    }
2309
2310    #[test]
2311    fn test_changeset_multi_table_encode_decode() {
2312        let mut session = Session::new();
2313        session.attach_table("a", 1, vec![true]);
2314        session.attach_table("b", 2, vec![true, false]);
2315        session.record_insert("a", vec![ChangesetValue::Integer(1)]);
2316        session.record_insert(
2317            "b",
2318            vec![
2319                ChangesetValue::Integer(2),
2320                ChangesetValue::Text("x".to_owned()),
2321            ],
2322        );
2323        session.record_delete("a", vec![ChangesetValue::Integer(3)]);
2324
2325        let cs = session.changeset();
2326        let encoded = cs.encode();
2327        let decoded = Changeset::decode(&encoded).unwrap();
2328        assert_eq!(decoded, cs);
2329    }
2330
2331    // -----------------------------------------------------------------------
2332    // Apply: additional conflict scenarios
2333    // -----------------------------------------------------------------------
2334
2335    #[test]
2336    fn test_apply_update_data_conflict_replace() {
2337        let mut target = SimpleTarget::default();
2338        target.tables.insert(
2339            "t".to_owned(),
2340            vec![vec![
2341                SqliteValue::Integer(1),
2342                SqliteValue::Text("actual".to_owned()),
2343            ]],
2344        );
2345
2346        let cs = Changeset {
2347            tables: vec![TableChangeset {
2348                info: TableInfo {
2349                    name: "t".to_owned(),
2350                    column_count: 2,
2351                    pk_flags: vec![true, false],
2352                },
2353                rows: vec![ChangesetRow {
2354                    op: ChangeOp::Update,
2355                    old_values: vec![
2356                        ChangesetValue::Integer(1),
2357                        ChangesetValue::Text("expected".to_owned()),
2358                    ],
2359                    new_values: vec![
2360                        ChangesetValue::Undefined,
2361                        ChangesetValue::Text("new".to_owned()),
2362                    ],
2363                }],
2364            }],
2365        };
2366
2367        let outcome = target.apply(&cs, |_, _| ConflictAction::Replace);
2368        assert_eq!(
2369            outcome,
2370            ApplyOutcome::Success {
2371                applied: 1,
2372                skipped: 0
2373            }
2374        );
2375        assert_eq!(
2376            target.tables["t"][0][1],
2377            SqliteValue::Text("new".to_owned())
2378        );
2379    }
2380
2381    #[test]
2382    fn test_apply_delete_data_conflict_replace_removes() {
2383        let mut target = SimpleTarget::default();
2384        target.tables.insert(
2385            "t".to_owned(),
2386            vec![vec![
2387                SqliteValue::Integer(1),
2388                SqliteValue::Text("actual".to_owned()),
2389            ]],
2390        );
2391
2392        let cs = Changeset {
2393            tables: vec![TableChangeset {
2394                info: TableInfo {
2395                    name: "t".to_owned(),
2396                    column_count: 2,
2397                    pk_flags: vec![true, false],
2398                },
2399                rows: vec![ChangesetRow {
2400                    op: ChangeOp::Delete,
2401                    old_values: vec![
2402                        ChangesetValue::Integer(1),
2403                        ChangesetValue::Text("expected".to_owned()),
2404                    ],
2405                    new_values: Vec::new(),
2406                }],
2407            }],
2408        };
2409
2410        let outcome = target.apply(&cs, |_, _| ConflictAction::Replace);
2411        assert_eq!(
2412            outcome,
2413            ApplyOutcome::Success {
2414                applied: 1,
2415                skipped: 0
2416            }
2417        );
2418        assert!(target.tables["t"].is_empty());
2419    }
2420
2421    #[test]
2422    fn test_apply_update_not_found_abort() {
2423        let mut target = SimpleTarget::default();
2424        let cs = Changeset {
2425            tables: vec![TableChangeset {
2426                info: TableInfo {
2427                    name: "t".to_owned(),
2428                    column_count: 1,
2429                    pk_flags: vec![true],
2430                },
2431                rows: vec![ChangesetRow {
2432                    op: ChangeOp::Update,
2433                    old_values: vec![ChangesetValue::Integer(1)],
2434                    new_values: vec![ChangesetValue::Integer(2)],
2435                }],
2436            }],
2437        };
2438        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
2439        assert_eq!(outcome, ApplyOutcome::Aborted { applied: 0 });
2440    }
2441
2442    #[test]
2443    fn test_apply_multiple_rows_mixed() {
2444        let mut target = SimpleTarget::default();
2445        let cs = Changeset {
2446            tables: vec![TableChangeset {
2447                info: TableInfo {
2448                    name: "t".to_owned(),
2449                    column_count: 2,
2450                    pk_flags: vec![true, false],
2451                },
2452                rows: vec![
2453                    ChangesetRow {
2454                        op: ChangeOp::Insert,
2455                        old_values: Vec::new(),
2456                        new_values: vec![
2457                            ChangesetValue::Integer(1),
2458                            ChangesetValue::Text("a".to_owned()),
2459                        ],
2460                    },
2461                    ChangesetRow {
2462                        op: ChangeOp::Insert,
2463                        old_values: Vec::new(),
2464                        new_values: vec![
2465                            ChangesetValue::Integer(2),
2466                            ChangesetValue::Text("b".to_owned()),
2467                        ],
2468                    },
2469                ],
2470            }],
2471        };
2472        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
2473        assert_eq!(
2474            outcome,
2475            ApplyOutcome::Success {
2476                applied: 2,
2477                skipped: 0
2478            }
2479        );
2480        assert_eq!(target.tables["t"].len(), 2);
2481    }
2482
2483    #[test]
2484    fn test_apply_empty_changeset() {
2485        let mut target = SimpleTarget::default();
2486        let cs = Changeset::new();
2487        let outcome = target.apply(&cs, |_, _| ConflictAction::Abort);
2488        assert_eq!(
2489            outcome,
2490            ApplyOutcome::Success {
2491                applied: 0,
2492                skipped: 0
2493            }
2494        );
2495    }
2496
2497    // -----------------------------------------------------------------------
2498    // TableChangeset encoding
2499    // -----------------------------------------------------------------------
2500
2501    #[test]
2502    fn test_table_changeset_encode_patchset() {
2503        let tc = TableChangeset {
2504            info: TableInfo {
2505                name: "t".to_owned(),
2506                column_count: 2,
2507                pk_flags: vec![true, false],
2508            },
2509            rows: vec![ChangesetRow {
2510                op: ChangeOp::Insert,
2511                old_values: Vec::new(),
2512                new_values: vec![ChangesetValue::Integer(1), ChangesetValue::Null],
2513            }],
2514        };
2515        let mut cs_buf = Vec::new();
2516        tc.encode_changeset(&mut cs_buf);
2517        let mut ps_buf = Vec::new();
2518        tc.encode_patchset(&mut ps_buf);
2519        // For INSERT, patchset = changeset
2520        assert_eq!(cs_buf, ps_buf);
2521    }
2522
2523    // -----------------------------------------------------------------------
2524    // changeset_varint_len
2525    // -----------------------------------------------------------------------
2526
2527    #[test]
2528    fn test_changeset_varint_len_values() {
2529        assert_eq!(changeset_varint_len(0), 1);
2530        assert_eq!(changeset_varint_len(127), 1);
2531        assert_eq!(changeset_varint_len(128), 2);
2532        assert!(changeset_varint_len(u64::MAX) > 0);
2533    }
2534
2535    // -----------------------------------------------------------------------
2536    // ConflictType / ConflictAction traits
2537    // -----------------------------------------------------------------------
2538
2539    #[test]
2540    fn test_conflict_type_eq() {
2541        assert_eq!(ConflictType::Data, ConflictType::Data);
2542        assert_ne!(ConflictType::Data, ConflictType::NotFound);
2543        assert_ne!(ConflictType::Conflict, ConflictType::Constraint);
2544        assert_ne!(ConflictType::Constraint, ConflictType::ForeignKey);
2545    }
2546
2547    #[test]
2548    fn test_conflict_action_eq() {
2549        assert_eq!(ConflictAction::OmitChange, ConflictAction::OmitChange);
2550        assert_ne!(ConflictAction::OmitChange, ConflictAction::Replace);
2551        assert_ne!(ConflictAction::Replace, ConflictAction::Abort);
2552    }
2553
2554    #[test]
2555    fn test_conflict_type_debug() {
2556        assert_eq!(format!("{:?}", ConflictType::ForeignKey), "ForeignKey");
2557    }
2558
2559    // -----------------------------------------------------------------------
2560    // Extension name
2561    // -----------------------------------------------------------------------
2562
2563    #[test]
2564    fn test_extension_name_value() {
2565        assert_eq!(extension_name(), "session");
2566    }
2567
2568    // -----------------------------------------------------------------------
2569    // ApplyOutcome
2570    // -----------------------------------------------------------------------
2571
2572    #[test]
2573    fn test_apply_outcome_debug() {
2574        let outcome = ApplyOutcome::Success {
2575            applied: 5,
2576            skipped: 2,
2577        };
2578        let s = format!("{:?}", outcome);
2579        assert!(s.contains('5'));
2580        assert!(s.contains('2'));
2581    }
2582
2583    #[test]
2584    fn test_apply_outcome_aborted_eq() {
2585        assert_eq!(
2586            ApplyOutcome::Aborted { applied: 3 },
2587            ApplyOutcome::Aborted { applied: 3 }
2588        );
2589        assert_ne!(
2590            ApplyOutcome::Aborted { applied: 3 },
2591            ApplyOutcome::Aborted { applied: 4 }
2592        );
2593    }
2594}