use std::{
    collections::{HashMap, VecDeque},
    sync::Arc,
};

use turso_core::{types::WalFrameInfo, LimboError, StepResult};

use crate::{
    database_replay_generator::{DatabaseReplayGenerator, ReplayInfo},
    database_sync_operations::WAL_FRAME_HEADER,
    errors::Error,
    types::{
        Coro, DatabaseChange, DatabaseChangeType, DatabaseTapeOperation, DatabaseTapeRowChangeType,
        SyncEngineIoResult,
    },
    wal_session::WalSession,
    Result,
};

/// Simple wrapper over [turso::Database] which extends its intereface with few methods
/// to collect changes made to the database and apply/revert arbitrary changes to the database
pub struct DatabaseTape {
    inner: Arc<turso_core::Database>,
    cdc_table: Arc<String>,
    pragma_query: String,
    cdc_version: std::sync::RwLock<Option<turso_core::CdcVersion>>,
}

const DEFAULT_CDC_TABLE_NAME: &str = "turso_cdc";
const DEFAULT_CDC_MODE: &str = "full";
const DEFAULT_CHANGES_BATCH_SIZE: usize = 100;
pub const CDC_PRAGMA_NAME: &str = "capture_data_changes_conn";

#[derive(Debug, Clone)]
pub struct DatabaseTapeOpts {
    pub cdc_table: Option<String>,
    pub cdc_mode: Option<String>,
}

pub(crate) async fn run_stmt_once<'a, Ctx>(
    coro: &'_ Coro<Ctx>,
    stmt: &'a mut turso_core::Statement,
) -> Result<Option<&'a turso_core::Row>> {
    loop {
        match stmt.step()? {
            StepResult::IO => {
                coro.yield_(SyncEngineIoResult::IO).await?;
            }
            StepResult::Done => {
                return Ok(None);
            }
            StepResult::Interrupt => {
                return Err(Error::DatabaseTapeError(
                    "statement was interrupted".to_string(),
                ))
            }
            StepResult::Busy => {
                return Err(Error::DatabaseTapeError("database is busy".to_string()))
            }
            StepResult::Row => return Ok(Some(stmt.row().unwrap())),
        }
    }
}

pub(crate) async fn run_stmt_expect_one_row<Ctx>(
    coro: &Coro<Ctx>,
    stmt: &mut turso_core::Statement,
) -> Result<Option<Vec<turso_core::Value>>> {
    let Some(row) = run_stmt_once(coro, stmt).await? else {
        return Ok(None);
    };
    let values = row.get_values().cloned().collect();
    let None = run_stmt_once(coro, stmt).await? else {
        return Err(Error::DatabaseTapeError("single row expected".to_string()));
    };
    Ok(Some(values))
}

pub(crate) async fn run_stmt_ignore_rows<Ctx>(
    coro: &Coro<Ctx>,
    stmt: &mut turso_core::Statement,
) -> Result<()> {
    while run_stmt_once(coro, stmt).await?.is_some() {}
    Ok(())
}

pub(crate) async fn exec_stmt<Ctx>(
    coro: &Coro<Ctx>,
    stmt: &mut turso_core::Statement,
) -> Result<()> {
    loop {
        match stmt.step()? {
            StepResult::IO => {
                coro.yield_(SyncEngineIoResult::IO).await?;
            }
            StepResult::Done => {
                return Ok(());
            }
            StepResult::Interrupt => {
                return Err(Error::DatabaseTapeError(
                    "statement was interrupted".to_string(),
                ))
            }
            StepResult::Busy => {
                return Err(Error::DatabaseTapeError("database is busy".to_string()))
            }
            StepResult::Row => panic!("statement should not return any rows"),
        }
    }
}

impl DatabaseTape {
    pub fn new(database: Arc<turso_core::Database>) -> Self {
        let opts = DatabaseTapeOpts {
            cdc_table: None,
            cdc_mode: None,
        };
        Self::new_with_opts(database, opts)
    }
    pub fn new_with_opts(database: Arc<turso_core::Database>, opts: DatabaseTapeOpts) -> Self {
        tracing::debug!("create local sync database with options {:?}", opts);
        let cdc_table_name = opts.cdc_table.unwrap_or(DEFAULT_CDC_TABLE_NAME.to_string());
        let cdc_mode = opts.cdc_mode.unwrap_or(DEFAULT_CDC_MODE.to_string());
        let pragma_query = format!("PRAGMA {CDC_PRAGMA_NAME}('{cdc_mode},{cdc_table_name}')");
        Self {
            inner: database,
            cdc_table: Arc::new(cdc_table_name.to_string()),
            pragma_query,
            cdc_version: std::sync::RwLock::new(None),
        }
    }
    pub(crate) fn connect_untracked(&self) -> Result<Arc<turso_core::Connection>> {
        let connection = self.inner.connect()?;
        Ok(connection)
    }
    pub async fn connect<Ctx>(&self, coro: &Coro<Ctx>) -> Result<Arc<turso_core::Connection>> {
        let connection = self.inner.connect()?;
        tracing::debug!("set '{CDC_PRAGMA_NAME}' for new connection");
        let mut stmt = connection.prepare(&self.pragma_query)?;
        run_stmt_ignore_rows(coro, &mut stmt).await?;
        // Cache CDC version from turso_cdc_version table
        if self.cdc_version.read().unwrap().is_none() {
            let version = Self::read_cdc_version(coro, &connection, &self.cdc_table).await?;
            *self.cdc_version.write().unwrap() = Some(version);
        }
        Ok(connection)
    }

    async fn read_cdc_version<Ctx>(
        coro: &Coro<Ctx>,
        connection: &Arc<turso_core::Connection>,
        cdc_table: &str,
    ) -> Result<turso_core::CdcVersion> {
        let query =
            format!("SELECT version FROM turso_cdc_version WHERE table_name = '{cdc_table}'");
        let mut stmt = match connection.prepare(&query) {
            Ok(stmt) => stmt,
            Err(turso_core::LimboError::ParseError(err)) if err.contains("no such table") => {
                return Ok(turso_core::CdcVersion::V1)
            }
            Err(err) => return Err(err.into()),
        };
        match run_stmt_expect_one_row(coro, &mut stmt).await? {
            Some(row) if !row.is_empty() => {
                if let turso_core::Value::Text(text) = &row[0] {
                    text.to_string()
                        .parse()
                        .map_err(|e: turso_core::LimboError| {
                            Error::DatabaseTapeError(e.to_string())
                        })
                } else {
                    Ok(turso_core::CdcVersion::V1)
                }
            }
            _ => Ok(turso_core::CdcVersion::V1),
        }
    }

    /// Builds an iterator which emits [DatabaseTapeOperation] by extracting data from CDC table
    pub fn iterate_changes(
        &self,
        opts: DatabaseChangesIteratorOpts,
    ) -> Result<DatabaseChangesIterator> {
        tracing::debug!("opening changes iterator with options {:?}", opts);
        let conn = self.inner.connect()?;

        let cdc_version = self
            .cdc_version
            .read()
            .unwrap()
            .expect("tape must be connected before iterate changes");

        Ok(DatabaseChangesIterator {
            conn,
            cdc_table: self.cdc_table.clone(),
            cdc_version,
            first_change_id: opts.first_change_id,
            batch: VecDeque::with_capacity(opts.batch_size),
            query_stmt: None,
            txn_boundary_returned: false,
            mode: opts.mode,
            batch_size: opts.batch_size,
            ignore_schema_changes: opts.ignore_schema_changes,
        })
    }
    /// Start raw WAL edit session which can append or rollback pages directly in the current WAL
    pub async fn start_wal_session<Ctx>(&self, coro: &Coro<Ctx>) -> Result<DatabaseWalSession> {
        let conn = self.connect(coro).await?;
        let mut wal_session = WalSession::new(conn);
        wal_session.begin()?;
        DatabaseWalSession::new(coro, wal_session).await
    }

    /// Start replay session which can apply [DatabaseTapeOperation] from [Self::iterate_changes]
    pub async fn start_replay_session<Ctx>(
        &self,
        coro: &Coro<Ctx>,
        opts: DatabaseReplaySessionOpts,
    ) -> Result<DatabaseReplaySession> {
        tracing::debug!("opening replay session");
        let conn = self.connect(coro).await?;
        conn.execute("BEGIN IMMEDIATE")?;
        Ok(DatabaseReplaySession {
            conn: conn.clone(),
            cached_delete_stmt: HashMap::new(),
            cached_insert_stmt: HashMap::new(),
            cached_update_stmt: HashMap::new(),
            in_txn: true,
            generator: DatabaseReplayGenerator { conn, opts },
        })
    }
}

pub struct DatabaseWalSession {
    page_size: usize,
    next_wal_frame_no: u64,
    pub wal_session: WalSession,
    prepared_frame: Option<(u32, Vec<u8>)>,
}

impl DatabaseWalSession {
    pub async fn new<Ctx>(coro: &Coro<Ctx>, wal_session: WalSession) -> Result<Self> {
        let conn = wal_session.conn();
        let frames_count = conn.wal_state()?.max_frame;
        let mut page_size_stmt = conn.prepare("PRAGMA page_size")?;
        let Some(row) = run_stmt_expect_one_row(coro, &mut page_size_stmt).await? else {
            return Err(Error::DatabaseTapeError(
                "unable to get database page size".to_string(),
            ));
        };
        if row.len() != 1 {
            return Err(Error::DatabaseTapeError(
                "unexpected columns count for PRAGMA page_size query".to_string(),
            ));
        }
        let turso_core::Value::Numeric(turso_core::Numeric::Integer(page_size)) = row[0] else {
            return Err(Error::DatabaseTapeError(
                "unexpected column type for PRAGMA page_size query".to_string(),
            ));
        };
        Ok(Self {
            page_size: page_size as usize,
            next_wal_frame_no: frames_count + 1,
            wal_session,
            prepared_frame: None,
        })
    }

    pub fn frames_count(&self) -> Result<u64> {
        Ok(self.wal_session.conn().wal_state()?.max_frame)
    }

    pub fn append_page(&mut self, page_no: u32, page: &[u8]) -> Result<()> {
        if page.len() != self.page_size {
            return Err(Error::DatabaseTapeError(format!(
                "page.len() must be equal to page_size: {} != {}",
                page.len(),
                self.page_size
            )));
        }
        self.flush_prepared_frame(0)?;

        let mut frame = vec![0u8; WAL_FRAME_HEADER + self.page_size];
        frame[WAL_FRAME_HEADER..].copy_from_slice(page);
        self.prepared_frame = Some((page_no, frame));

        Ok(())
    }

    pub async fn rollback_page<Ctx>(
        &mut self,
        coro: &Coro<Ctx>,
        page_no: u32,
        frame_watermark: u64,
    ) -> Result<()> {
        self.flush_prepared_frame(0)?;

        let conn = self.wal_session.conn();
        let mut frame = vec![0u8; WAL_FRAME_HEADER + self.page_size];
        let begin_read_result =
            conn.try_wal_watermark_read_page_begin(page_no, Some(frame_watermark))?;
        let end_read_result = match begin_read_result {
            Some((page_ref, c)) => {
                while !c.succeeded() {
                    let _ = coro.yield_(SyncEngineIoResult::IO).await;
                }
                conn.try_wal_watermark_read_page_end(&mut frame[WAL_FRAME_HEADER..], page_ref)?
            }
            None => false,
        };
        if end_read_result {
            tracing::trace!("rollback page {}", page_no);
            self.prepared_frame = Some((page_no, frame));
        } else {
            tracing::trace!(
                "skip rollback page {} as no page existed with given watermark",
                page_no
            );
        }

        Ok(())
    }

    pub async fn rollback_changes_after<Ctx>(
        &mut self,
        coro: &Coro<Ctx>,
        frame_watermark: u64,
    ) -> Result<usize> {
        let conn = self.wal_session.conn();
        let pages = conn.wal_changed_pages_after(frame_watermark)?;
        tracing::info!("rolling back {} pages", pages.len());
        let pages_cnt = pages.len();
        for page_no in pages {
            self.rollback_page(coro, page_no, frame_watermark).await?;
        }
        Ok(pages_cnt)
    }

    pub fn db_size(&self) -> Result<u32> {
        let frames_count = self.frames_count()?;
        let conn = self.wal_session.conn();
        let mut page = vec![0u8; self.page_size];
        assert!(conn.try_wal_watermark_read_page(1, &mut page, Some(frames_count))?);
        let db_size = u32::from_be_bytes(page[28..32].try_into().unwrap());
        Ok(db_size)
    }

    pub fn commit(&mut self, db_size: u32) -> Result<()> {
        self.flush_prepared_frame(db_size)
    }

    fn flush_prepared_frame(&mut self, db_size: u32) -> Result<()> {
        let Some((page_no, mut frame)) = self.prepared_frame.take() else {
            return Ok(());
        };

        let frame_info = WalFrameInfo { db_size, page_no };
        frame_info.put_to_frame_header(&mut frame);

        let frame_no = self.next_wal_frame_no;
        tracing::debug!(
            "flush prepared frame {:?} as frame_no {}",
            frame_info,
            frame_no
        );
        self.wal_session.conn().wal_insert_frame(frame_no, &frame)?;
        self.next_wal_frame_no += 1;

        Ok(())
    }
}

#[derive(Debug, Clone, Copy)]
pub enum DatabaseChangesIteratorMode {
    Apply,
    Revert,
}

impl DatabaseChangesIteratorMode {
    pub fn query(&self, table_name: &str, limit: usize) -> String {
        let (operation, order) = match self {
            DatabaseChangesIteratorMode::Apply => (">=", "ASC"),
            DatabaseChangesIteratorMode::Revert => ("<=", "DESC"),
        };
        format!(
            "SELECT * FROM {table_name} WHERE change_id {operation} ? ORDER BY change_id {order} LIMIT {limit}",
        )
    }
    pub fn first_id(&self) -> i64 {
        match self {
            DatabaseChangesIteratorMode::Apply => -1,
            DatabaseChangesIteratorMode::Revert => i64::MAX,
        }
    }
    pub fn next_id(&self, id: i64) -> i64 {
        match self {
            DatabaseChangesIteratorMode::Apply => id + 1,
            DatabaseChangesIteratorMode::Revert => id - 1,
        }
    }
}

#[derive(Debug, Clone)]
pub struct DatabaseChangesIteratorOpts {
    pub first_change_id: Option<i64>,
    pub batch_size: usize,
    pub mode: DatabaseChangesIteratorMode,
    pub ignore_schema_changes: bool,
}

impl Default for DatabaseChangesIteratorOpts {
    fn default() -> Self {
        Self {
            first_change_id: None,
            batch_size: DEFAULT_CHANGES_BATCH_SIZE,
            mode: DatabaseChangesIteratorMode::Apply,
            ignore_schema_changes: true,
        }
    }
}

pub struct DatabaseChangesIterator {
    conn: Arc<turso_core::Connection>,
    cdc_table: Arc<String>,
    cdc_version: turso_core::CdcVersion,
    query_stmt: Option<turso_core::Statement>,
    first_change_id: Option<i64>,
    batch: VecDeque<DatabaseTapeOperation>,
    txn_boundary_returned: bool,
    mode: DatabaseChangesIteratorMode,
    batch_size: usize,
    ignore_schema_changes: bool,
}

const SQLITE_SCHEMA_TABLE: &str = "sqlite_schema";
impl DatabaseChangesIterator {
    pub async fn next<Ctx>(&mut self, coro: &Coro<Ctx>) -> Result<Option<DatabaseTapeOperation>> {
        if self.batch.is_empty() {
            self.refill(coro).await?;
        }
        loop {
            let next = if let Some(op) = self.batch.pop_front() {
                self.txn_boundary_returned = matches!(op, DatabaseTapeOperation::Commit);
                Some(op)
            } else if !self.txn_boundary_returned {
                // For v1 (no explicit COMMIT records), emit a synthetic Commit at end of batch.
                // For v2, COMMIT records are already in the batch, but we also emit a final
                // synthetic one at end-of-table for safety.
                self.txn_boundary_returned = true;
                Some(DatabaseTapeOperation::Commit)
            } else {
                None
            };
            if let Some(DatabaseTapeOperation::RowChange(change)) = &next {
                if self.ignore_schema_changes && change.table_name == SQLITE_SCHEMA_TABLE {
                    continue;
                }
            }
            return Ok(next);
        }
    }
    async fn refill<Ctx>(&mut self, coro: &Coro<Ctx>) -> Result<()> {
        if self.query_stmt.is_none() {
            let query = self.mode.query(&self.cdc_table, self.batch_size);
            let stmt = match self.conn.prepare(&query) {
                Ok(stmt) => stmt,
                Err(LimboError::ParseError(err)) if err.contains("no such table") => return Ok(()),
                Err(err) => return Err(err.into()),
            };
            self.query_stmt = Some(stmt);
        }
        let query_stmt = self.query_stmt.as_mut().unwrap();

        let change_id_filter = self.first_change_id.unwrap_or(self.mode.first_id());
        query_stmt.reset()?;
        query_stmt.bind_at(
            1.try_into().unwrap(),
            turso_core::Value::from_i64(change_id_filter),
        );

        let mut last_change_id = None;
        while let Some(row) = run_stmt_once(coro, query_stmt).await? {
            let database_change = DatabaseChange::from_row(row, self.cdc_version)?;
            last_change_id = Some(database_change.change_id);
            if database_change.change_type == DatabaseChangeType::Commit {
                self.batch.push_back(DatabaseTapeOperation::Commit);
            } else {
                let tape_change = match self.mode {
                    DatabaseChangesIteratorMode::Apply => database_change.into_apply()?,
                    DatabaseChangesIteratorMode::Revert => database_change.into_revert()?,
                };
                self.batch
                    .push_back(DatabaseTapeOperation::RowChange(tape_change));
            }
        }
        if let Some(change_id) = last_change_id {
            self.first_change_id = Some(self.mode.next_id(change_id));
        }
        Ok(())
    }
}

#[derive(Clone)]
pub struct DatabaseReplaySessionOpts {
    pub use_implicit_rowid: bool,
}

impl std::fmt::Debug for DatabaseReplaySessionOpts {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DatabaseReplaySessionOpts")
            .field("use_implicit_rowid", &self.use_implicit_rowid)
            .finish()
    }
}

pub(crate) struct CachedStmt {
    stmt: turso_core::Statement,
    info: ReplayInfo,
}

pub struct DatabaseReplaySession {
    pub(crate) conn: Arc<turso_core::Connection>,
    pub(crate) cached_delete_stmt: HashMap<String, CachedStmt>,
    pub(crate) cached_insert_stmt: HashMap<(String, usize), CachedStmt>,
    pub(crate) cached_update_stmt: HashMap<(String, Vec<bool>), CachedStmt>,
    pub(crate) in_txn: bool,
    pub(crate) generator: DatabaseReplayGenerator,
}

async fn replay_stmt<Ctx>(
    coro: &Coro<Ctx>,
    stmt: &mut turso_core::Statement,
    values: Vec<turso_core::Value>,
) -> Result<()> {
    stmt.reset()?;
    for (i, value) in values.into_iter().enumerate() {
        stmt.bind_at((i + 1).try_into().unwrap(), value);
    }
    exec_stmt(coro, stmt).await?;
    Ok(())
}

impl DatabaseReplaySession {
    pub fn conn(&self) -> Arc<turso_core::Connection> {
        self.conn.clone()
    }
    pub async fn replay<Ctx>(
        &mut self,
        coro: &Coro<Ctx>,
        operation: DatabaseTapeOperation,
    ) -> Result<()> {
        match operation {
            DatabaseTapeOperation::Commit => {
                tracing::debug!("replay: commit replayed changes after transaction boundary");
                if self.in_txn {
                    self.conn.execute("COMMIT")?;
                    self.in_txn = false;
                }
            }
            DatabaseTapeOperation::StmtReplay(replay) => {
                let mut stmt = self.conn.prepare(&replay.sql)?;
                replay_stmt(coro, &mut stmt, replay.values).await?;
                return Ok(());
            }
            DatabaseTapeOperation::RowChange(change) => {
                if !self.in_txn {
                    tracing::trace!("replay: start txn for replaying changes");
                    self.conn.execute("BEGIN IMMEDIATE")?;
                    self.in_txn = true;
                }
                let table = &change.table_name;
                let change_type = (&change.change).into();

                if table == SQLITE_SCHEMA_TABLE {
                    let replay_info = self.generator.replay_info(coro, &change).await?;
                    self.conn.execute(replay_info.query.as_str())?;
                } else {
                    match change.change {
                        DatabaseTapeRowChangeType::Delete { before } => {
                            let key = self.populate_delete_stmt(coro, table).await?;
                            tracing::trace!(
                                "ready to use prepared delete statement for replay: key={}",
                                key
                            );
                            let cached = self.cached_delete_stmt.get_mut(key).unwrap();
                            cached.stmt.reset()?;
                            let values = self.generator.replay_values(
                                &cached.info,
                                change_type,
                                change.id,
                                before,
                                None,
                            );
                            replay_stmt(coro, &mut cached.stmt, values).await?;
                        }
                        DatabaseTapeRowChangeType::Insert { after } => {
                            let key = self.populate_insert_stmt(coro, table, after.len()).await?;
                            tracing::trace!(
                                "ready to use prepared insert statement for replay: key={:?}",
                                key
                            );
                            let cached = self.cached_insert_stmt.get_mut(&key).unwrap();
                            cached.stmt.reset()?;
                            let values = self.generator.replay_values(
                                &cached.info,
                                change_type,
                                change.id,
                                after,
                                None,
                            );
                            replay_stmt(coro, &mut cached.stmt, values).await?;
                        }
                        DatabaseTapeRowChangeType::Update {
                            after,
                            updates: Some(updates),
                            ..
                        } => {
                            assert!(updates.len() % 2 == 0);
                            let columns_cnt = updates.len() / 2;
                            let mut columns = Vec::with_capacity(columns_cnt);
                            for value in updates.iter().take(columns_cnt) {
                                columns.push(match value {
                                    turso_core::Value::Numeric(turso_core::Numeric::Integer(x @ (1 | 0))) => *x > 0,
                                    _ => panic!("unexpected 'changes' binary record first-half component: {value:?}")
                                });
                            }
                            let key = self.populate_update_stmt(coro, table, &columns).await?;
                            tracing::trace!(
                                "ready to use prepared update statement for replay: key={:?}",
                                key
                            );
                            let cached = self.cached_update_stmt.get_mut(&key).unwrap();
                            cached.stmt.reset()?;
                            let values = self.generator.replay_values(
                                &cached.info,
                                change_type,
                                change.id,
                                after,
                                Some(updates),
                            );
                            replay_stmt(coro, &mut cached.stmt, values).await?;
                        }
                        DatabaseTapeRowChangeType::Update {
                            before,
                            after,
                            updates: None,
                        } => {
                            let key = self.populate_delete_stmt(coro, table).await?;
                            tracing::trace!(
                                "ready to use prepared delete statement for replay of update: key={:?}",
                                key
                            );
                            let cached = self.cached_delete_stmt.get_mut(key).unwrap();
                            cached.stmt.reset()?;
                            let values = self.generator.replay_values(
                                &cached.info,
                                DatabaseChangeType::Delete,
                                change.id,
                                before,
                                None,
                            );
                            replay_stmt(coro, &mut cached.stmt, values).await?;

                            let key = self.populate_insert_stmt(coro, table, after.len()).await?;
                            tracing::trace!(
                                "ready to use prepared insert statement for replay of update: key={:?}",
                                key
                            );
                            let cached = self.cached_insert_stmt.get_mut(&key).unwrap();
                            cached.stmt.reset()?;
                            let values = self.generator.replay_values(
                                &cached.info,
                                DatabaseChangeType::Insert,
                                change.id,
                                after,
                                None,
                            );
                            replay_stmt(coro, &mut cached.stmt, values).await?;
                        }
                    }
                }
            }
        }
        Ok(())
    }
    async fn populate_delete_stmt<'a, Ctx>(
        &mut self,
        coro: &Coro<Ctx>,
        table: &'a str,
    ) -> Result<&'a str> {
        if self.cached_delete_stmt.contains_key(table) {
            return Ok(table);
        }
        tracing::trace!("prepare delete statement for replay: table={}", table);
        let info = self.generator.delete_query(coro, table).await?;
        let stmt = self.conn.prepare(&info.query)?;
        self.cached_delete_stmt
            .insert(table.to_string(), CachedStmt { stmt, info });
        Ok(table)
    }
    async fn populate_insert_stmt<Ctx>(
        &mut self,
        coro: &Coro<Ctx>,
        table: &str,
        columns: usize,
    ) -> Result<(String, usize)> {
        let key = (table.to_string(), columns);
        if self.cached_insert_stmt.contains_key(&key) {
            return Ok(key);
        }
        tracing::trace!(
            "prepare insert statement for replay: table={}, columns={}",
            table,
            columns
        );
        let info = self.generator.upsert_query(coro, table, columns).await?;
        let stmt = self.conn.prepare(&info.query)?;
        self.cached_insert_stmt
            .insert(key.clone(), CachedStmt { stmt, info });
        Ok(key)
    }
    async fn populate_update_stmt<Ctx>(
        &mut self,
        coro: &Coro<Ctx>,
        table: &str,
        columns: &[bool],
    ) -> Result<(String, Vec<bool>)> {
        let key = (table.to_string(), columns.to_owned());
        if self.cached_update_stmt.contains_key(&key) {
            return Ok(key);
        }
        tracing::trace!("prepare update statement for replay: table={}", table);
        let info = self.generator.update_query(coro, table, columns).await?;
        let stmt = self.conn.prepare(&info.query)?;
        self.cached_update_stmt
            .insert(key.clone(), CachedStmt { stmt, info });
        Ok(key)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use tempfile::NamedTempFile;

    use crate::{
        database_tape::{
            run_stmt_once, DatabaseChangesIteratorOpts, DatabaseReplaySessionOpts, DatabaseTape,
        },
        types::{Coro, DatabaseTapeOperation, DatabaseTapeRowChange, DatabaseTapeRowChangeType},
    };

    #[test]
    pub fn test_database_tape_connect() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());
        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));
        let mut gen = genawaiter::sync::Gen::new({
            let db1 = db1.clone();
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn = db1.connect(&coro).await.unwrap();
                let mut stmt = conn.prepare("SELECT * FROM turso_cdc").unwrap();
                let mut rows = Vec::new();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                rows
            }
        });
        let rows = loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => break result,
            }
        };
        assert_eq!(rows, vec![] as Vec<Vec<turso_core::Value>>);
    }

    #[test]
    pub fn test_database_tape_iterate_changes() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());
        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let mut gen = genawaiter::sync::Gen::new({
            let db1 = db1.clone();
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn = db1.connect(&coro).await.unwrap();
                conn.execute("CREATE TABLE t(x)").unwrap();
                conn.execute("INSERT INTO t VALUES (1), (2), (3)").unwrap();
                let opts = Default::default();
                let mut iterator = db1.iterate_changes(opts).unwrap();
                let mut changes = Vec::new();
                while let Some(change) = iterator.next(&coro).await.unwrap() {
                    changes.push(change);
                }
                changes
            }
        });
        let changes = loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => break result,
            }
        };
        tracing::info!("changes: {:?}", changes);
        assert_eq!(changes.len(), 5);
        // CREATE TABLE emits a COMMIT record (schema INSERT is filtered by ignore_schema_changes)
        assert!(matches!(changes[0], DatabaseTapeOperation::Commit));
        assert!(matches!(
            changes[1],
            DatabaseTapeOperation::RowChange(DatabaseTapeRowChange {
                change_id: 3,
                id: 1,
                ref table_name,
                change: DatabaseTapeRowChangeType::Insert { .. },
                ..
            }) if table_name == "t"
        ));
        assert!(matches!(
            changes[2],
            DatabaseTapeOperation::RowChange(DatabaseTapeRowChange {
                change_id: 4,
                id: 2,
                ref table_name,
                change: DatabaseTapeRowChangeType::Insert { .. },
                ..
            }) if table_name == "t"
        ));
        assert!(matches!(
            changes[3],
            DatabaseTapeOperation::RowChange(DatabaseTapeRowChange {
                change_id: 5,
                id: 3,
                ref table_name,
                change: DatabaseTapeRowChangeType::Insert { .. },
                ..
            }) if table_name == "t"
        ));
        assert!(matches!(changes[4], DatabaseTapeOperation::Commit));
    }

    #[test]
    pub fn test_database_tape_replay_changes_preserve_rowid() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());
        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            let db1 = db1.clone();
            let db2 = db2.clone();
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1.execute("CREATE TABLE t(x)").unwrap();
                conn1
                    .execute("INSERT INTO t(rowid, x) VALUES (10, 1), (20, 2)")
                    .unwrap();
                let conn2 = db2.connect(&coro).await.unwrap();
                conn2.execute("CREATE TABLE t(x)").unwrap();
                conn2
                    .execute("INSERT INTO t(rowid, x) VALUES (1, -1), (2, -2)")
                    .unwrap();

                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: true,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();
                    let opts = Default::default();
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut stmt = conn2.prepare("SELECT rowid, x FROM t").unwrap();
                let mut rows = Vec::new();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                rows
            }
        });
        let rows = loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(rows) => break rows,
            }
        };
        tracing::info!("rows: {:?}", rows);
        assert_eq!(
            rows,
            vec![
                vec![
                    turso_core::Value::from_i64(1),
                    turso_core::Value::from_i64(-1)
                ],
                vec![
                    turso_core::Value::from_i64(2),
                    turso_core::Value::from_i64(-2)
                ],
                vec![
                    turso_core::Value::from_i64(10),
                    turso_core::Value::from_i64(1)
                ],
                vec![
                    turso_core::Value::from_i64(20),
                    turso_core::Value::from_i64(2)
                ]
            ]
        );
    }

    #[test]
    pub fn test_database_tape_replay_changes_do_not_preserve_rowid() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());
        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            let db1 = db1.clone();
            let db2 = db2.clone();
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1.execute("CREATE TABLE t(x)").unwrap();
                conn1
                    .execute("INSERT INTO t(rowid, x) VALUES (10, 1), (20, 2)")
                    .unwrap();
                let conn2 = db2.connect(&coro).await.unwrap();
                conn2.execute("CREATE TABLE t(x)").unwrap();
                conn2
                    .execute("INSERT INTO t(rowid, x) VALUES (1, -1), (2, -2)")
                    .unwrap();

                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();
                    let opts = Default::default();
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut stmt = conn2.prepare("SELECT rowid, x FROM t").unwrap();
                let mut rows = Vec::new();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                rows
            }
        });
        let rows = loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(rows) => break rows,
            }
        };
        tracing::info!("rows: {:?}", rows);
        assert_eq!(
            rows,
            vec![
                vec![
                    turso_core::Value::from_i64(1),
                    turso_core::Value::from_i64(-1)
                ],
                vec![
                    turso_core::Value::from_i64(2),
                    turso_core::Value::from_i64(-2)
                ],
                vec![
                    turso_core::Value::from_i64(3),
                    turso_core::Value::from_i64(1)
                ],
                vec![
                    turso_core::Value::from_i64(4),
                    turso_core::Value::from_i64(2)
                ]
            ]
        );
    }

    #[test]
    pub fn test_database_tape_replay_changes_delete() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());
        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            let db1 = db1.clone();
            let db2 = db2.clone();
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1.execute("CREATE TABLE t(x TEXT PRIMARY KEY)").unwrap();
                conn1.execute("INSERT INTO t(x) VALUES ('a')").unwrap();
                conn1.execute("DELETE FROM t").unwrap();
                let conn2 = db2.connect(&coro).await.unwrap();
                conn2.execute("CREATE TABLE t(x TEXT PRIMARY KEY)").unwrap();
                conn2.execute("INSERT INTO t(x) VALUES ('b')").unwrap();

                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();
                    let opts = Default::default();
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut stmt = conn2.prepare("SELECT rowid, x FROM t").unwrap();
                let mut rows = Vec::new();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                rows
            }
        });
        let rows = loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(rows) => break rows,
            }
        };
        tracing::info!("rows: {:?}", rows);
        assert_eq!(
            rows,
            vec![vec![
                turso_core::Value::from_i64(1),
                turso_core::Value::Text(turso_core::types::Text::new("b"))
            ]]
        );
    }

    #[test]
    pub fn test_database_tape_replay_schema_changes() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();
        let temp_file3 = NamedTempFile::new().unwrap();
        let db_path3 = temp_file3.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let db3 = turso_core::Database::open_file(io.clone(), db_path3).unwrap();
        let db3 = Arc::new(DatabaseTape::new(db3));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t(x, y) VALUES ('a', 10)")
                    .unwrap();
                let conn2 = db2.connect(&coro).await.unwrap();
                conn2
                    .execute("CREATE TABLE q(x TEXT PRIMARY KEY, y)")
                    .unwrap();
                conn2
                    .execute("INSERT INTO q(x, y) VALUES ('b', 20)")
                    .unwrap();

                let conn3 = db3.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db3.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: false,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                    let mut iterator = db2.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut rows = Vec::new();
                let mut stmt = conn3.prepare("SELECT rowid, x, y FROM t").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![vec![
                        turso_core::Value::from_i64(1),
                        turso_core::Value::Text(turso_core::types::Text::new("a")),
                        turso_core::Value::from_i64(10),
                    ]]
                );

                let mut rows = Vec::new();
                let mut stmt = conn3.prepare("SELECT rowid, x, y FROM q").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![vec![
                        turso_core::Value::from_i64(1),
                        turso_core::Value::Text(turso_core::types::Text::new("b")),
                        turso_core::Value::from_i64(20),
                    ]]
                );
                let mut rows = Vec::new();
                let mut stmt = conn3
                    .prepare(
                        "SELECT * FROM sqlite_schema WHERE name NOT IN ('turso_cdc', 'turso_cdc_version') AND type = 'table'",
                    )
                    .unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("table")),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "sqlite_sequence"
                            )),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "sqlite_sequence"
                            )),
                            turso_core::Value::from_i64(2),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "CREATE TABLE sqlite_sequence(name,seq)"
                            )),
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("table")),
                            turso_core::Value::Text(turso_core::types::Text::new("t")),
                            turso_core::Value::Text(turso_core::types::Text::new("t")),
                            turso_core::Value::from_i64(6),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "CREATE TABLE t (x TEXT PRIMARY KEY, y)"
                            )),
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("table")),
                            turso_core::Value::Text(turso_core::types::Text::new("q")),
                            turso_core::Value::Text(turso_core::types::Text::new("q")),
                            turso_core::Value::from_i64(8),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "CREATE TABLE q (x TEXT PRIMARY KEY, y)"
                            )),
                        ]
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    #[test]
    pub fn test_database_tape_replay_create_index() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y)")
                    .unwrap();
                conn1.execute("CREATE INDEX t_idx ON t(y)").unwrap();

                let conn2 = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: false,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut rows = Vec::new();
                let mut stmt = conn2
                    .prepare("SELECT * FROM sqlite_schema WHERE name IN ('t', 't_idx')")
                    .unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("table")),
                            turso_core::Value::Text(turso_core::types::Text::new("t")),
                            turso_core::Value::Text(turso_core::types::Text::new("t")),
                            turso_core::Value::from_i64(6),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "CREATE TABLE t (x TEXT PRIMARY KEY, y)"
                            )),
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("index")),
                            turso_core::Value::Text(turso_core::types::Text::new("t_idx")),
                            turso_core::Value::Text(turso_core::types::Text::new("t")),
                            turso_core::Value::from_i64(8),
                            turso_core::Value::Text(turso_core::types::Text::new(
                                "CREATE INDEX IF NOT EXISTS t_idx ON t (y)"
                            )),
                        ]
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    #[test]
    pub fn test_database_tape_replay_alter_table() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y)")
                    .unwrap();
                conn1.execute("ALTER TABLE t ADD COLUMN z").unwrap();
                conn1.execute("ALTER TABLE t DROP COLUMN y").unwrap();

                let conn2 = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: false,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut rows = Vec::new();
                let mut stmt = conn2
                    .prepare("SELECT * FROM sqlite_schema WHERE name IN ('t')")
                    .unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![vec![
                        turso_core::Value::Text(turso_core::types::Text::new("table")),
                        turso_core::Value::Text(turso_core::types::Text::new("t")),
                        turso_core::Value::Text(turso_core::types::Text::new("t")),
                        turso_core::Value::from_i64(6),
                        turso_core::Value::Text(turso_core::types::Text::new(
                            "CREATE TABLE t (x TEXT PRIMARY KEY, z)"
                        )),
                    ]]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    #[test]
    pub fn test_database_tape_replay_non_overlapping_updates() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();
        let temp_file3 = NamedTempFile::new().unwrap();
        let db_path3 = temp_file3.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let db3 = turso_core::Database::open_file(io.clone(), db_path3).unwrap();
        let db3 = Arc::new(DatabaseTape::new(db3));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y, z)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('turso', 1, 2)")
                    .unwrap();
                conn1
                    .execute("UPDATE t SET y = 10 WHERE x = 'turso'")
                    .unwrap();

                let conn2 = db2.connect_untracked().unwrap();
                conn2
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y, z)")
                    .unwrap();
                conn2
                    .execute("INSERT INTO t VALUES ('turso', 1, 2)")
                    .unwrap();

                let conn2 = db2.connect(&coro).await.unwrap();
                conn2
                    .execute("UPDATE t SET z = 20 WHERE x = 'turso'")
                    .unwrap();

                let conn3 = db3.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db3.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: false,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }

                    let mut iterator = db2.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut rows = Vec::new();
                let mut stmt = conn3.prepare("SELECT * FROM t").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![vec![
                        turso_core::Value::Text(turso_core::types::Text::new("turso")),
                        turso_core::Value::from_i64(10),
                        turso_core::Value::from_i64(20),
                    ]]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    #[test]
    pub fn test_database_tape_replay_ddl_changes_idempotent() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();
        let temp_file3 = NamedTempFile::new().unwrap();
        let db_path3 = temp_file3.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let db3 = turso_core::Database::open_file(io.clone(), db_path3).unwrap();
        let db3 = Arc::new(DatabaseTape::new(db3));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y, z)")
                    .unwrap();
                conn1.execute("CREATE INDEX t_idx ON t(y, z)").unwrap();

                let conn2 = db2.connect(&coro).await.unwrap();
                conn2
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y, z)")
                    .unwrap();
                conn2.execute("CREATE INDEX t_idx ON t(y, z)").unwrap();

                let conn3 = db3.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db3.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: false,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        tracing::info!("1. operation: {:?}", operation);
                        session.replay(&coro, operation).await.unwrap();
                    }

                    let mut iterator = db2.iterate_changes(opts.clone()).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        tracing::info!("2. operation: {:?}", operation);
                        session.replay(&coro, operation).await.unwrap();
                    }
                }
                let mut rows = Vec::new();
                let mut stmt = conn3.prepare("SELECT name FROM sqlite_master").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_value(0).to_text().unwrap().to_string());
                }
                assert_eq!(
                    rows,
                    vec![
                        "sqlite_sequence".to_string(),
                        "turso_cdc".to_string(),
                        "turso_cdc_version".to_string(),
                        "sqlite_autoindex_turso_cdc_version_1".to_string(),
                        "t".to_string(),
                        "sqlite_autoindex_t_1".to_string(),
                        "t_idx".to_string()
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    // Tests for the "explicitly list table columns in the replay generator" commit.
    // These test that CDC records captured before ALTER TABLE ADD COLUMN can be
    // correctly replayed into a schema that has the extra column.

    /// Bootstrap from empty: CREATE TABLE → INSERT → ALTER TABLE ADD COLUMN → INSERT.
    /// Target DB starts empty and receives all changes (including DDL) via replay.
    /// Verifies schema has new column and all data rows are correct.
    #[test]
    pub fn test_database_tape_replay_alter_table_add_column_after_inserts() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('a', 'alpha')")
                    .unwrap();
                conn1.execute("INSERT INTO t VALUES ('b', 'beta')").unwrap();
                conn1
                    .execute("ALTER TABLE t ADD COLUMN z TEXT DEFAULT NULL")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('c', 'gamma', 'extra')")
                    .unwrap();

                let conn2 = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: false,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }

                // Verify schema
                let mut stmt = conn2
                    .prepare("SELECT sql FROM sqlite_schema WHERE name = 't'")
                    .unwrap();
                let row = run_stmt_once(&coro, &mut stmt).await.unwrap().unwrap();
                let sql = row.get_value(0).to_text().unwrap().to_string();
                assert!(
                    sql.contains("z"),
                    "schema should contain z column after ALTER TABLE: {sql}"
                );

                // Verify data
                let mut rows = Vec::new();
                let mut stmt = conn2.prepare("SELECT x, y, z FROM t ORDER BY x").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("a")),
                            turso_core::Value::Text(turso_core::types::Text::new("alpha")),
                            turso_core::Value::Null,
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("b")),
                            turso_core::Value::Text(turso_core::types::Text::new("beta")),
                            turso_core::Value::Null,
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("c")),
                            turso_core::Value::Text(turso_core::types::Text::new("gamma")),
                            turso_core::Value::Text(turso_core::types::Text::new("extra")),
                        ],
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    /// Pre-ALTER INSERT records replayed into a target that already has the post-ALTER schema.
    /// Source: CREATE TABLE t(x PK, y) → INSERT 2 rows (2 cols each).
    /// Target: already has t(x PK, y, z) — the post-ALTER schema.
    /// Replay with ignore_schema_changes: true (data only).
    /// Without the fix, INSERT INTO t VALUES (?,?) fails on a 3-column table.
    /// With the fix, INSERT INTO t(x, y) VALUES (?,?) works.
    #[test]
    pub fn test_database_tape_replay_pre_alter_inserts_into_post_alter_schema() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                // Source: pre-ALTER schema with 2 columns
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('a', 'alpha')")
                    .unwrap();
                conn1.execute("INSERT INTO t VALUES ('b', 'beta')").unwrap();

                // Target: post-ALTER schema with 3 columns (set up without CDC)
                let conn2 = db2.connect_untracked().unwrap();
                conn2
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT, z TEXT DEFAULT NULL)")
                    .unwrap();

                let _conn2_tracked = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: true,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }

                // Verify data — pre-ALTER rows should have NULL for the new column
                let mut rows = Vec::new();
                let mut stmt = conn2.prepare("SELECT x, y, z FROM t ORDER BY x").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("a")),
                            turso_core::Value::Text(turso_core::types::Text::new("alpha")),
                            turso_core::Value::Null,
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("b")),
                            turso_core::Value::Text(turso_core::types::Text::new("beta")),
                            turso_core::Value::Null,
                        ],
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    /// Pre-ALTER UPDATE records replayed into post-ALTER target schema.
    /// Source: CREATE TABLE t(x PK, y) → INSERT → UPDATE y.
    /// Target: already has t(x PK, y, z) — the post-ALTER schema with existing data.
    /// Replay with ignore_schema_changes: true (data only).
    /// Without the fix, update_query indexes out of bounds into the `columns` bool slice.
    /// With the fix, only the columns present in the CDC record are referenced.
    #[test]
    pub fn test_database_tape_replay_pre_alter_updates_into_post_alter_schema() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                // Source: pre-ALTER schema — insert + update
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('a', 'alpha')")
                    .unwrap();
                conn1
                    .execute("UPDATE t SET y = 'ALPHA' WHERE x = 'a'")
                    .unwrap();

                // Target: post-ALTER schema with the row already present
                let conn2 = db2.connect_untracked().unwrap();
                conn2
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT, z TEXT DEFAULT NULL)")
                    .unwrap();
                conn2
                    .execute("INSERT INTO t VALUES ('a', 'alpha', 'z-val')")
                    .unwrap();

                let _conn2_tracked = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: true,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }

                // Verify: y should be updated to 'ALPHA', z should stay 'z-val'
                let mut rows = Vec::new();
                let mut stmt = conn2.prepare("SELECT x, y, z FROM t ORDER BY x").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![vec![
                        turso_core::Value::Text(turso_core::types::Text::new("a")),
                        turso_core::Value::Text(turso_core::types::Text::new("ALPHA")),
                        turso_core::Value::Text(turso_core::types::Text::new("z-val")),
                    ]]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    /// Mixed pre-ALTER and post-ALTER CDC records replayed into post-ALTER target.
    /// Source: CREATE TABLE → INSERT (2 cols) → ALTER TABLE ADD COLUMN → INSERT (3 cols) → UPDATE (3 cols).
    /// Target: already has post-ALTER schema. Replay data only.
    /// Tests that both pre-ALTER (2-col) and post-ALTER (3-col) records work correctly.
    #[test]
    pub fn test_database_tape_replay_mixed_pre_and_post_alter_records() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                let conn1 = db1.connect(&coro).await.unwrap();
                // Pre-ALTER: 2 columns
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('a', 'alpha')")
                    .unwrap();
                // ALTER TABLE — adds z column
                conn1
                    .execute("ALTER TABLE t ADD COLUMN z TEXT DEFAULT NULL")
                    .unwrap();
                // Post-ALTER: 3 columns
                conn1
                    .execute("INSERT INTO t VALUES ('b', 'beta', 'b-extra')")
                    .unwrap();
                conn1
                    .execute("UPDATE t SET z = 'a-extra' WHERE x = 'a'")
                    .unwrap();

                // Target: post-ALTER schema (set up without CDC tracking)
                let conn2 = db2.connect_untracked().unwrap();
                conn2
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT, z TEXT DEFAULT NULL)")
                    .unwrap();

                let _conn2_tracked = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: true,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }

                // Verify all rows
                let mut rows = Vec::new();
                let mut stmt = conn2.prepare("SELECT x, y, z FROM t ORDER BY x").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("a")),
                            turso_core::Value::Text(turso_core::types::Text::new("alpha")),
                            turso_core::Value::Text(turso_core::types::Text::new("a-extra")),
                        ],
                        vec![
                            turso_core::Value::Text(turso_core::types::Text::new("b")),
                            turso_core::Value::Text(turso_core::types::Text::new("beta")),
                            turso_core::Value::Text(turso_core::types::Text::new("b-extra")),
                        ],
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    /// Pre-ALTER DELETE records replayed into post-ALTER target schema.
    /// Source: CREATE TABLE t(x PK, y) → INSERT → DELETE.
    /// Target: already has t(x PK, y, z) with the row present.
    /// Replay data changes — delete should work via PK regardless of column count mismatch.
    #[test]
    pub fn test_database_tape_replay_pre_alter_deletes_into_post_alter_schema() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                // Source: pre-ALTER schema — insert then delete
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT)")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t VALUES ('a', 'alpha')")
                    .unwrap();
                conn1.execute("INSERT INTO t VALUES ('b', 'beta')").unwrap();
                conn1.execute("DELETE FROM t WHERE x = 'a'").unwrap();

                // Target: post-ALTER schema with both rows present
                let conn2 = db2.connect_untracked().unwrap();
                conn2
                    .execute("CREATE TABLE t(x TEXT PRIMARY KEY, y TEXT, z TEXT DEFAULT NULL)")
                    .unwrap();
                conn2
                    .execute("INSERT INTO t VALUES ('a', 'alpha', 'z1')")
                    .unwrap();
                conn2
                    .execute("INSERT INTO t VALUES ('b', 'beta', 'z2')")
                    .unwrap();

                let _conn2_tracked = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: false,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: true,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }

                // Verify: 'a' should be upserted then deleted, 'b' upserted.
                // The pre-ALTER upsert for 'b' uses ON CONFLICT(x) DO UPDATE SET x=.., y=..
                // which doesn't touch z, so the pre-existing z='z2' is preserved.
                let mut rows = Vec::new();
                let mut stmt = conn2.prepare("SELECT x, y, z FROM t ORDER BY x").unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![vec![
                        turso_core::Value::Text(turso_core::types::Text::new("b")),
                        turso_core::Value::Text(turso_core::types::Text::new("beta")),
                        turso_core::Value::Text(turso_core::types::Text::new("z2")),
                    ]]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }

    /// Pre-ALTER INSERT with use_implicit_rowid=true into post-ALTER target.
    /// Tests the rowid-preserving path: INSERT INTO t(col1, col2, rowid) VALUES (?,?,?).
    #[test]
    pub fn test_database_tape_replay_pre_alter_inserts_preserve_rowid() {
        let temp_file1 = NamedTempFile::new().unwrap();
        let db_path1 = temp_file1.path().to_str().unwrap();
        let temp_file2 = NamedTempFile::new().unwrap();
        let db_path2 = temp_file2.path().to_str().unwrap();

        let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new().unwrap());

        let db1 = turso_core::Database::open_file(io.clone(), db_path1).unwrap();
        let db1 = Arc::new(DatabaseTape::new(db1));

        let db2 = turso_core::Database::open_file(io.clone(), db_path2).unwrap();
        let db2 = Arc::new(DatabaseTape::new(db2));

        let mut gen = genawaiter::sync::Gen::new({
            |coro| async move {
                let coro: Coro<()> = coro.into();
                // Source: no explicit PK, uses implicit rowid
                let conn1 = db1.connect(&coro).await.unwrap();
                conn1.execute("CREATE TABLE t(a TEXT, b TEXT)").unwrap();
                conn1
                    .execute("INSERT INTO t(rowid, a, b) VALUES (10, 'x', 'y')")
                    .unwrap();
                conn1
                    .execute("INSERT INTO t(rowid, a, b) VALUES (20, 'p', 'q')")
                    .unwrap();

                // Target: post-ALTER schema with extra column
                let conn2 = db2.connect_untracked().unwrap();
                conn2
                    .execute("CREATE TABLE t(a TEXT, b TEXT, c TEXT DEFAULT NULL)")
                    .unwrap();

                let _conn2_tracked = db2.connect(&coro).await.unwrap();
                {
                    let opts = DatabaseReplaySessionOpts {
                        use_implicit_rowid: true,
                    };
                    let mut session = db2.start_replay_session(&coro, opts).await.unwrap();

                    let opts = DatabaseChangesIteratorOpts {
                        ignore_schema_changes: true,
                        ..Default::default()
                    };
                    let mut iterator = db1.iterate_changes(opts).unwrap();
                    while let Some(operation) = iterator.next(&coro).await.unwrap() {
                        session.replay(&coro, operation).await.unwrap();
                    }
                }

                // Verify data — rowids should be preserved
                let mut rows = Vec::new();
                let mut stmt = conn2
                    .prepare("SELECT rowid, a, b, c FROM t ORDER BY rowid")
                    .unwrap();
                while let Some(row) = run_stmt_once(&coro, &mut stmt).await.unwrap() {
                    rows.push(row.get_values().cloned().collect::<Vec<_>>());
                }
                assert_eq!(
                    rows,
                    vec![
                        vec![
                            turso_core::Value::from_i64(10),
                            turso_core::Value::Text(turso_core::types::Text::new("x")),
                            turso_core::Value::Text(turso_core::types::Text::new("y")),
                            turso_core::Value::Null,
                        ],
                        vec![
                            turso_core::Value::from_i64(20),
                            turso_core::Value::Text(turso_core::types::Text::new("p")),
                            turso_core::Value::Text(turso_core::types::Text::new("q")),
                            turso_core::Value::Null,
                        ],
                    ]
                );
                crate::Result::Ok(())
            }
        });
        loop {
            match gen.resume_with(Ok(())) {
                genawaiter::GeneratorState::Yielded(..) => io.step().unwrap(),
                genawaiter::GeneratorState::Complete(result) => {
                    result.unwrap();
                    break;
                }
            }
        }
    }
}
