Skip to main content

teaql_provider_sqlite/
lib.rs

1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::future::Future;
3use std::pin::Pin;
4use std::str::FromStr;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone};
8use rusqlite::types::{Value as SqliteValue, ValueRef};
9use rusqlite::{
10    Connection, OptionalExtension, Row, functions::FunctionFlags, params, params_from_iter,
11};
12use rust_decimal::Decimal;
13use teaql_core::{
14    CompactRow, DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, SelectQuery,
15    UpdateCommand, Value,
16};
17use teaql_runtime::{
18    GraphNode, InternalIdGenerator, RawAuditEvent, RuntimeError, SchemaProvider, UserContext,
19    canonical_id_space_entity,
20};
21use teaql_sql::{
22    CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
23    quote_identifier_if_needed,
24};
25
26pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
27pub const DEFAULT_PREPARED_STATEMENT_CACHE_CAPACITY: usize = 64;
28pub const DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY: usize = 64;
29
30#[derive(Debug, Default, Clone, Copy)]
31pub struct SqliteDialect;
32
33impl SqlDialect for SqliteDialect {
34    fn kind(&self) -> DatabaseKind {
35        DatabaseKind::Sqlite
36    }
37
38    fn quote_ident(&self, ident: &str) -> String {
39        quote_ident(ident)
40    }
41
42    fn placeholder(&self, _index: usize) -> String {
43        "?".to_owned()
44    }
45
46    fn prefers_small_parent_relation_probes(&self) -> bool {
47        true
48    }
49
50    fn schema_type_sql(
51        &self,
52        data_type: DataType,
53        property: &PropertyDescriptor,
54    ) -> Result<&'static str, SqlCompileError> {
55        match data_type {
56            DataType::Bool => Ok("BOOLEAN"),
57            DataType::I64 | DataType::U64 if property.is_id => Ok("INTEGER"),
58            DataType::I64 | DataType::U64 => Ok("INTEGER"),
59            DataType::F64 => Ok("REAL"),
60            DataType::Decimal => Ok("NUMERIC"),
61            DataType::Text => Ok("VARCHAR(255)"),
62            DataType::LargeText => Ok("TEXT"),
63            DataType::Json => Ok("JSON"),
64            DataType::Date => Ok("DATE"),
65            DataType::Timestamp => Ok("TIMESTAMP"),
66        }
67    }
68
69    fn compile_add_column(
70        &self,
71        entity: &EntityDescriptor,
72        property: &PropertyDescriptor,
73    ) -> Result<String, SqlCompileError> {
74        // SQLite does not support adding NOT NULL columns without a DEFAULT.
75        // Since TeaQL enforces nullability at the application layer, we can safely
76        // strip the NOT NULL constraint when adding columns to existing tables.
77        let def = self.column_definition_sql(property)?;
78        let def_without_not_null = def.replace(" NOT NULL", "");
79
80        Ok(format!(
81            "ALTER TABLE {} ADD COLUMN {}",
82            self.quote_ident(&entity.table_name),
83            def_without_not_null
84        ))
85    }
86}
87
88#[derive(Debug)]
89pub enum MutationExecutorError {
90    Sqlite(rusqlite::Error),
91    SqlCompile(SqlCompileError),
92    UnsupportedValue(&'static str),
93    UnsupportedColumnType(String),
94    Bind(String),
95    Lock(String),
96}
97
98impl std::fmt::Display for MutationExecutorError {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            Self::Sqlite(err) => err.fmt(f),
102            Self::SqlCompile(err) => err.fmt(f),
103            Self::UnsupportedValue(kind) => {
104                write!(
105                    f,
106                    "unsupported rusqlite bind value for mutation executor: {kind}"
107                )
108            }
109            Self::UnsupportedColumnType(kind) => {
110                write!(
111                    f,
112                    "unsupported rusqlite column type for record decoding: {kind}"
113                )
114            }
115            Self::Bind(message) => write!(f, "rusqlite bind error: {message}"),
116            Self::Lock(message) => write!(f, "rusqlite connection lock error: {message}"),
117        }
118    }
119}
120
121impl std::error::Error for MutationExecutorError {}
122
123impl From<rusqlite::Error> for MutationExecutorError {
124    fn from(value: rusqlite::Error) -> Self {
125        Self::Sqlite(value)
126    }
127}
128
129impl From<SqlCompileError> for MutationExecutorError {
130    fn from(value: SqlCompileError) -> Self {
131        Self::SqlCompile(value)
132    }
133}
134
135#[derive(Clone)]
136pub struct SqliteMutationExecutor {
137    connection: Arc<Mutex<Connection>>,
138    column_layout_cache: Arc<Mutex<HashMap<String, Arc<ColumnLayout>>>>,
139}
140
141impl SqliteMutationExecutor {
142    pub fn new(connection: Arc<Mutex<Connection>>) -> Self {
143        if let Ok(connection) = connection.lock() {
144            connection
145                .set_prepared_statement_cache_capacity(DEFAULT_PREPARED_STATEMENT_CACHE_CAPACITY);
146        }
147        Self {
148            connection,
149            column_layout_cache: Arc::new(Mutex::new(HashMap::new())),
150        }
151    }
152
153    pub fn from_connection(connection: Connection) -> Self {
154        Self::new(Arc::new(Mutex::new(connection)))
155    }
156
157    pub fn connection(&self) -> Arc<Mutex<Connection>> {
158        Arc::clone(&self.connection)
159    }
160
161    pub fn ensure_schema(
162        &self,
163        dialect: &SqliteDialect,
164        entities: &[&EntityDescriptor],
165    ) -> Result<(), MutationExecutorError> {
166        self.ensure_soundex_function()?;
167        self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
168
169        for entity in entities {
170            if !self.table_exists(&entity.table_name)? {
171                let sql = dialect.compile_create_table(entity)?;
172                self.lock()?.execute(&sql, [])?;
173                continue;
174            }
175
176            let existing_columns = self.table_columns(&entity.table_name)?;
177            for property in &entity.properties {
178                let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
179                if existing_columns.contains(&bare_column) {
180                    continue;
181                }
182                let sql = dialect.compile_add_column(entity, property)?;
183                self.lock()?.execute(&sql, [])?;
184            }
185
186            for sql in dialect.schema_indexes_sqls(entity)? {
187                self.lock()?.execute(&sql, [])?;
188            }
189        }
190        self.clear_query_caches();
191        Ok(())
192    }
193
194    fn ensure_soundex_function(&self) -> Result<(), MutationExecutorError> {
195        self.lock()?.create_scalar_function(
196            "soundex",
197            1,
198            FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
199            |ctx| {
200                let input = ctx.get_raw(0).as_str().ok();
201                Ok(sqlite_compatible_soundex(input))
202            },
203        )?;
204        Ok(())
205    }
206
207    fn clear_query_caches(&self) {
208        if let Ok(connection) = self.connection.lock() {
209            connection.flush_prepared_statement_cache();
210        }
211        if let Ok(mut cache) = self.column_layout_cache.lock() {
212            cache.clear();
213        }
214    }
215
216    pub fn ensure_id_space_table(&self, table_name: &str) -> Result<(), MutationExecutorError> {
217        let sql = format!(
218            "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
219            quote_ident(table_name)
220        );
221        self.lock()?.execute(&sql, [])?;
222        Ok(())
223    }
224
225    pub fn begin_transaction(&self) -> Result<(), MutationExecutorError> {
226        self.lock()?.execute("BEGIN IMMEDIATE", [])?;
227        Ok(())
228    }
229
230    pub fn commit_transaction(&self) -> Result<(), MutationExecutorError> {
231        self.lock()?.execute("COMMIT", [])?;
232        Ok(())
233    }
234
235    pub fn rollback_transaction(&self) -> Result<(), MutationExecutorError> {
236        self.lock()?.execute("ROLLBACK", [])?;
237        Ok(())
238    }
239
240    pub fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
241        let params = bind_values(&query.params)?;
242        let rows = self
243            .lock()?
244            .execute(&query.sql_with_comment(), params_from_iter(params.iter()))?;
245        Ok(rows as u64)
246    }
247
248    pub fn fetch_all_compact(
249        &self,
250        query: &CompiledQuery,
251    ) -> Result<Vec<CompactRow>, MutationExecutorError> {
252        let params = bind_values(&query.params)?;
253        let connection = self.lock()?;
254        let sql = query.sql_with_comment();
255        let mut statement = connection.prepare_cached(&sql)?;
256        let layout = cached_column_layout(&self.column_layout_cache, &query.sql, &statement);
257        let mut rows = statement.query(params_from_iter(params.iter()))?;
258        let mut result = Vec::new();
259        while let Some(row) = rows.next()? {
260            result.push(CompactRow::new(
261                layout.names.clone(),
262                decode_sqlite_values(row, &layout.columns)?,
263            ));
264        }
265        Ok(result)
266    }
267
268    /// Fetch rows in streaming mode (chunked).
269    /// Returns a Vec of StreamChunk, each containing up to `chunk_size` rows.
270    pub fn fetch_stream(
271        &self,
272        query: &CompiledQuery,
273        chunk_size: usize,
274    ) -> Result<Vec<teaql_data_service::StreamChunk>, MutationExecutorError> {
275        let params = bind_values(&query.params)?;
276        let connection = self.lock()?;
277        let sql = query.sql_with_comment();
278        let mut statement = connection.prepare_cached(&sql)?;
279        let layout = cached_column_layout(&self.column_layout_cache, &query.sql, &statement);
280        let mut rows = statement.query(params_from_iter(params.iter()))?;
281
282        let mut chunks = Vec::new();
283        let mut current_chunk = Vec::new();
284        let mut chunk_index = 0;
285
286        while let Some(row) = rows.next()? {
287            current_chunk.push(CompactRow::new(
288                layout.names.clone(),
289                decode_sqlite_values(row, &layout.columns)?,
290            ));
291            if current_chunk.len() >= chunk_size {
292                chunks.push(teaql_data_service::StreamChunk {
293                    rows: current_chunk,
294                    chunk_index,
295                    is_last: false,
296                });
297                current_chunk = Vec::new();
298                chunk_index += 1;
299            }
300        }
301
302        // Push the final chunk (may be empty if exactly aligned)
303        chunks.push(teaql_data_service::StreamChunk {
304            rows: current_chunk,
305            chunk_index,
306            is_last: true,
307        });
308
309        Ok(chunks)
310    }
311
312    pub fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
313        let exists: i64 = self.lock()?.query_row(
314            "SELECT COUNT(1) FROM sqlite_master WHERE type = 'table' AND name = ?",
315            [table_name],
316            |row| row.get(0),
317        )?;
318        Ok(exists > 0)
319    }
320
321    pub fn table_columns(
322        &self,
323        table_name: &str,
324    ) -> Result<BTreeSet<String>, MutationExecutorError> {
325        let pragma_sql = format!("PRAGMA table_info({})", quote_ident(table_name));
326        let connection = self.lock()?;
327        let mut statement = connection.prepare(&pragma_sql)?;
328        let rows = statement.query_map([], |row| row.get::<_, String>("name"))?;
329        let mut columns = BTreeSet::new();
330        for row in rows {
331            columns.insert(row?.to_lowercase());
332        }
333        Ok(columns)
334    }
335
336    fn lock(&self) -> Result<MutexGuard<'_, Connection>, MutationExecutorError> {
337        self.connection
338            .lock()
339            .map_err(|err| MutationExecutorError::Lock(err.to_string()))
340    }
341}
342
343fn sqlite_compatible_soundex(input: Option<&str>) -> String {
344    fn code(byte: u8) -> u8 {
345        match byte.to_ascii_uppercase() {
346            b'B' | b'F' | b'P' | b'V' => 1,
347            b'C' | b'G' | b'J' | b'K' | b'Q' | b'S' | b'X' | b'Z' => 2,
348            b'D' | b'T' => 3,
349            b'L' => 4,
350            b'M' | b'N' => 5,
351            b'R' => 6,
352            _ => 0,
353        }
354    }
355    let Some(input) = input else {
356        return "?000".to_owned();
357    };
358    let Some((first_index, first)) = input
359        .bytes()
360        .enumerate()
361        .find(|(_, byte)| byte.is_ascii_alphabetic())
362    else {
363        return "?000".to_owned();
364    };
365    let mut result = String::with_capacity(4);
366    result.push(char::from(first.to_ascii_uppercase()));
367    let mut previous = code(first);
368    for byte in input.bytes().skip(first_index + 1) {
369        if !byte.is_ascii_alphabetic() {
370            continue;
371        }
372        let current = code(byte);
373        if current != 0 && current != previous {
374            result.push(char::from(b'0' + current));
375            if result.len() == 4 {
376                break;
377            }
378        }
379        previous = current;
380    }
381    while result.len() < 4 {
382        result.push('0');
383    }
384    result
385}
386
387impl teaql_data_service::DataServiceExecutor for SqliteMutationExecutor {
388    type Error = MutationExecutorError;
389
390    fn capabilities(&self) -> teaql_data_service::DataServiceCapabilities {
391        teaql_data_service::DataServiceCapabilities {
392            query: true,
393            mutation: true,
394            transaction: true,
395            schema: true,
396            id_generation: true,
397            ..Default::default()
398        }
399    }
400}
401
402impl SqlTransport for SqliteMutationExecutor {
403    type Error = MutationExecutorError;
404
405    async fn fetch_all_compact_sql(
406        &self,
407        query: &CompiledQuery,
408    ) -> Result<Vec<CompactRow>, Self::Error> {
409        SqliteMutationExecutor::fetch_all_compact(self, query)
410    }
411
412    async fn fetch_repeated_compact_sql(
413        &self,
414        template: &CompiledQuery,
415        param_index: usize,
416        values: &[Value],
417    ) -> Result<Vec<CompactRow>, Self::Error> {
418        let connection = self.lock()?;
419        let sql = template.sql_with_comment();
420        let mut statement = connection.prepare_cached(&sql)?;
421        let layout = cached_column_layout(&self.column_layout_cache, &template.sql, &statement);
422        let mut result = Vec::new();
423        let mut query_params = template.params.clone();
424        for value in values {
425            query_params[param_index] = value.clone();
426            let params = bind_values(&query_params)?;
427            let mut rows = statement.query(params_from_iter(params.iter()))?;
428            while let Some(row) = rows.next()? {
429                result.push(CompactRow::new(
430                    layout.names.clone(),
431                    decode_sqlite_values(row, &layout.columns)?,
432                ));
433            }
434        }
435        Ok(result)
436    }
437
438    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
439        SqliteMutationExecutor::execute(self, query)
440    }
441}
442
443impl teaql_sql::StreamingSqlTransport for SqliteMutationExecutor {
444    // A rusqlite statement and its rows borrow the guarded connection for the
445    // lifetime of the stream. This stream is intentionally local/non-Send, so
446    // retaining the synchronous guard across yields is required and safe.
447    #[allow(clippy::await_holding_lock)]
448    fn stream_sql(
449        &self,
450        query: CompiledQuery,
451        chunk_size: usize,
452    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
453        let connection = self.connection.clone();
454        let column_layout_cache = self.column_layout_cache.clone();
455        Box::pin(async_stream::try_stream! {
456            let params = bind_values(&query.params)?;
457            let guard = connection.lock().map_err(|err| MutationExecutorError::Lock(err.to_string()))?;
458            let sql = query.sql_with_comment();
459            let mut statement = guard.prepare_cached(&sql)?;
460            let layout = cached_column_layout(&column_layout_cache, &query.sql, &statement);
461            let mut rows = statement.query(params_from_iter(params.iter()))?;
462            let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
463            while let Some(row) = rows.next()? {
464                chunk.push(CompactRow::new(layout.names.clone(), decode_sqlite_values(row, &layout.columns)?));
465                if chunk.len() == chunk_size { yield teaql_data_service::StreamChunk { rows: std::mem::take(&mut chunk), chunk_index: index, is_last: false }; index += 1; }
466            }
467            if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows: chunk, chunk_index: index, is_last: true }; }
468        })
469    }
470}
471
472impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
473    fn query_stream(
474        &self,
475        request: teaql_data_service::QueryRequest,
476        chunk_size: usize,
477    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
478        let dialect = SqliteDialect;
479        // Use a dummy entity descriptor for compilation
480        let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
481        match dialect.compile_select(&entity_desc, &request.query) {
482            Ok(compiled) => {
483                teaql_sql::StreamingSqlTransport::stream_sql(self, compiled, chunk_size)
484            }
485            Err(error) => Box::pin(futures_util::stream::once(async {
486                Err(MutationExecutorError::SqlCompile(error))
487            })),
488        }
489    }
490}
491
492impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
493    type Error = MutationExecutorError;
494
495    async fn commit_sql(self) -> Result<(), Self::Error> {
496        self.commit_transaction()
497    }
498
499    async fn rollback_sql(self) -> Result<(), Self::Error> {
500        self.rollback_transaction()
501    }
502}
503
504impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
505    type Tx<'a>
506        = Self
507    where
508        Self: 'a;
509
510    async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
511        self.begin_transaction()?;
512        Ok(self.clone())
513    }
514}
515
516fn initial_graph_row_sqlite(
517    executor: &SqliteMutationExecutor,
518    dialect: &SqliteDialect,
519    entity: &EntityDescriptor,
520    graph: &GraphNode,
521) -> Result<Option<teaql_core::CompactRow>, MutationExecutorError> {
522    let Some(id) = graph.values.get("id") else {
523        return Ok(None);
524    };
525    let mut select = SelectQuery::new(&graph.entity)
526        .filter(Expr::eq("id", id.clone()))
527        .limit(1);
528    for field in graph.values.keys() {
529        select = select.project(field);
530    }
531    if let Some(version) = entity
532        .version_property()
533        .filter(|version| !graph.values.contains_key(&version.name))
534    {
535        select = select.project(&version.name);
536    }
537    let query = dialect.compile_select(entity, &select)?;
538    Ok(executor.fetch_all_compact(&query)?.into_iter().next())
539}
540
541fn compile_initial_graph_insert(
542    dialect: &impl SqlDialect,
543    entity: &EntityDescriptor,
544    graph: &GraphNode,
545) -> Result<CompiledQuery, MutationExecutorError> {
546    let mut command = InsertCommand::new(&graph.entity);
547    for (field, value) in &graph.values {
548        command = command.value(field.clone(), value.clone());
549    }
550    dialect.compile_insert(entity, &command).map_err(Into::into)
551}
552
553fn compile_initial_graph_update(
554    dialect: &impl SqlDialect,
555    entity: &EntityDescriptor,
556    graph: &GraphNode,
557    current: &teaql_core::CompactRow,
558) -> Result<Option<CompiledQuery>, MutationExecutorError> {
559    let Some(id) = graph.values.get("id") else {
560        return Ok(None);
561    };
562    let mut command = UpdateCommand::new(&graph.entity, id.clone());
563    for (field, value) in &graph.values {
564        if field != "id"
565            && field != "version"
566            && !bootstrap_values_equal(current.get(field), Some(value))
567        {
568            command = command.value(field.clone(), value.clone());
569        }
570    }
571    if command.values.is_empty() {
572        return Ok(None);
573    }
574    if let Some(version) = entity
575        .version_property()
576        .and_then(|property| current.get(&property.name))
577        .and_then(Value::try_i64)
578    {
579        command = command.expected_version(version);
580    }
581    match dialect.compile_update(entity, &command) {
582        Ok(query) => Ok(Some(query)),
583        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
584        Err(err) => Err(err.into()),
585    }
586}
587
588fn bootstrap_values_equal(left: Option<&Value>, right: Option<&Value>) -> bool {
589    let (Some(left), Some(right)) = (left, right) else {
590        return left.is_none() && right.is_none();
591    };
592    if left == right {
593        return true;
594    }
595    matches!((left.try_decimal(), right.try_decimal()), (Some(a), Some(b)) if a == b)
596}
597
598pub(crate) fn ensure_sqlite_physical_schema_for(
599    context: &UserContext,
600) -> Result<(), MutationExecutorError> {
601    let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
602        MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
603    })?;
604    let executor = context
605        .get_resource::<SqliteMutationExecutor>()
606        .ok_or_else(|| {
607            MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
608        })?;
609
610    let entities = context.all_entities();
611
612    // Ensure id space table exists
613    executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
614
615    // Process each entity table individually with granular events
616    for entity in &entities {
617        let field_count = entity.properties.len();
618        if !executor.table_exists(&entity.table_name)? {
619            // New table: create it
620            let sql = dialect.compile_create_table(entity)?;
621            executor.lock()?.execute(&sql, [])?;
622            let _ = context.send_event(RawAuditEvent::schema_created(
623                &entity.name,
624                &entity.table_name,
625                field_count,
626            ));
627            continue;
628        }
629        // Existing table: check for missing columns
630        let existing_columns = executor.table_columns(&entity.table_name)?;
631        let mut fields_added = 0;
632        for property in &entity.properties {
633            let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
634            if existing_columns.contains(&bare_column) {
635                continue;
636            }
637            let sql = dialect.compile_add_column(entity, property)?;
638            executor.lock()?.execute(&sql, [])?;
639            let _ = context.send_event(RawAuditEvent::field_added(
640                &entity.name,
641                &entity.table_name,
642                &property.column_name,
643            ));
644            fields_added += 1;
645        }
646        let _ = context.send_event(RawAuditEvent::schema_verified(
647            &entity.name,
648            &entity.table_name,
649            field_count,
650        ));
651        let _ = fields_added; // used above for FieldAdded events
652    }
653
654    executor.clear_query_caches();
655    Ok(())
656}
657
658pub(crate) fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
659    ensure_sqlite_physical_schema_for(context)?;
660    if !context.initial_graphs().is_empty() || !context.root_graphs().is_empty() {
661        return Err(MutationExecutorError::Bind(
662            "generated root/constant bootstrap must use the typed RuntimeModule callback"
663                .to_owned(),
664        ));
665    }
666    Ok(())
667}
668
669#[allow(dead_code)]
670fn ensure_legacy_sqlite_bootstrap_for(context: &UserContext) -> Result<(), MutationExecutorError> {
671    ensure_sqlite_physical_schema_for(context)?;
672    let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
673        MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
674    })?;
675    let executor = context
676        .get_resource::<SqliteMutationExecutor>()
677        .ok_or_else(|| {
678            MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
679        })?;
680
681    // Constant graphs are reconciled so model changes are propagated.
682    let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
683    let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); // (inserted, updated)
684    for graph in context.initial_graphs() {
685        let entity = context.entity(&graph.entity).ok_or_else(|| {
686            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
687        })?;
688        let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
689        if let Some(current) = initial_graph_row_sqlite(executor, dialect, entity, graph)? {
690            if let Some(query) = compile_initial_graph_update(dialect, entity, graph, &current)? {
691                executor.execute(&query)?;
692                counts.1 += 1;
693            }
694            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
695                id_generator.ensure_floor(&graph.entity, id)?;
696            }
697            continue;
698        }
699        let query = compile_initial_graph_insert(dialect, entity, graph)?;
700        executor.execute(&query)?;
701        counts.0 += 1; // inserted
702        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
703            id_generator.ensure_floor(&graph.entity, id)?;
704        }
705    }
706
707    // Roots are create-if-absent. Once present, application-owned values win.
708    for graph in context.root_graphs() {
709        let entity = context.entity(&graph.entity).ok_or_else(|| {
710            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
711        })?;
712        if initial_graph_row_sqlite(executor, dialect, entity, graph)?.is_some() {
713            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
714                id_generator.ensure_floor(&graph.entity, id)?;
715            }
716            continue;
717        }
718        let query = compile_initial_graph_insert(dialect, entity, graph)?;
719        executor.execute(&query)?;
720        seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
721        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
722            id_generator.ensure_floor(&graph.entity, id)?;
723        }
724    }
725
726    // Fire DataSeeded events per entity type
727    for (entity_name, (inserted, updated)) in &seed_counts {
728        let entity = context.entity(entity_name).ok_or_else(|| {
729            MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
730        })?;
731        let _ = context.send_event(RawAuditEvent::data_seeded(
732            entity_name,
733            &entity.table_name,
734            *inserted,
735            *updated,
736        ));
737    }
738
739    executor.clear_query_caches();
740    Ok(())
741}
742
743#[derive(Debug, Default, Clone, Copy)]
744pub struct SqliteSchemaProvider;
745
746impl SchemaProvider for SqliteSchemaProvider {
747    fn ensure_schema<'a>(
748        &'a self,
749        context: &'a UserContext,
750        _invocation: &'a teaql_runtime::SchemaInvocation,
751    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
752        Box::pin(async move {
753            ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
754        })
755    }
756}
757
758pub trait SqliteProviderExt {
759    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
760}
761
762impl SqliteProviderExt for UserContext {
763    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
764        self.insert_resource(SqliteDialect);
765        self.insert_resource(executor);
766        self.set_schema_provider(SqliteSchemaProvider);
767        self
768    }
769}
770
771#[derive(Clone)]
772pub struct SqliteIdSpaceGenerator {
773    executor: SqliteMutationExecutor,
774    table_name: String,
775}
776
777impl SqliteIdSpaceGenerator {
778    pub fn new(connection: Connection) -> Self {
779        Self::from_executor(SqliteMutationExecutor::from_connection(connection))
780    }
781
782    pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
783        Self {
784            executor,
785            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
786        }
787    }
788
789    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
790        self.table_name = table_name.into();
791        self
792    }
793
794    pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
795        self.executor.ensure_id_space_table(&self.table_name)
796    }
797
798    pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
799        let entity = canonical_id_space_entity(entity);
800        let entity = entity.as_str();
801        self.ensure_table()?;
802        let table = quote_ident(&self.table_name);
803        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
804        let insert_sql = format!("INSERT INTO {table} (type_name, current_level) VALUES (?, 1)");
805        let update_sql = format!(
806            "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
807        );
808        for attempt in 1..=100 {
809            let connection = self.executor.lock()?;
810            let current = connection
811                .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
812                .optional()?;
813            if let Some(current) = current {
814                let next = current.checked_add(1).ok_or_else(|| {
815                    MutationExecutorError::Bind(format!(
816                        "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
817                    ))
818                })?;
819                if connection.execute(&update_sql, params![next, entity, current])? == 1 {
820                    return u64::try_from(next).map_err(|_| {
821                        MutationExecutorError::Bind(format!(
822                            "generated id {next} cannot be represented as u64"
823                        ))
824                    });
825                }
826            } else {
827                match connection.execute(&insert_sql, params![entity]) {
828                    Ok(1) => return Ok(1),
829                    Ok(changed) => {
830                        return Err(MutationExecutorError::Bind(format!(
831                            "ID space insert for {entity} changed {changed} rows"
832                        )));
833                    }
834                    Err(error)
835                        if error.sqlite_error_code()
836                            == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
837                    Err(error) => return Err(error.into()),
838                }
839            }
840        }
841        Err(MutationExecutorError::Bind(format!(
842            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
843        )))
844    }
845
846    pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
847        let entity = canonical_id_space_entity(entity);
848        let entity = entity.as_str();
849        self.ensure_table()?;
850        let floor = i64::try_from(floor).map_err(|_| {
851            MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
852        })?;
853        let table = quote_ident(&self.table_name);
854        for _ in 1..=100 {
855            let connection = self.executor.lock()?;
856            let current = connection
857                .query_row(
858                    &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
859                    [entity],
860                    |row| row.get::<_, i64>(0),
861                )
862                .optional()?;
863            match current {
864                Some(current) if current >= floor => return Ok(()),
865                Some(current) => {
866                    if connection.execute(
867                        &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
868                        params![floor, entity, current],
869                    )? == 1 { return Ok(()); }
870                }
871                None => match connection.execute(
872                    &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
873                    params![entity, floor],
874                ) {
875                    Ok(1) => return Ok(()),
876                    Ok(_) => {}
877                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
878                    Err(error) => return Err(error.into()),
879                },
880            }
881        }
882        Err(MutationExecutorError::Bind(format!(
883            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
884        )))
885    }
886}
887
888impl InternalIdGenerator for SqliteIdSpaceGenerator {
889    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
890        self.next_id(entity)
891            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
892    }
893
894    fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), RuntimeError> {
895        SqliteIdSpaceGenerator::ensure_floor(self, entity, floor)
896            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
897    }
898}
899
900fn quote_ident(ident: &str) -> String {
901    quote_identifier_if_needed(ident, '"')
902}
903
904/// Strip wrapping identifier quotes from a SQL identifier.
905///
906/// SQLite `PRAGMA table_info` returns bare column names (e.g. `description`),
907/// but generated `PropertyDescriptor::column_name` may carry quotes
908/// (e.g. `"description"`) when the name is a reserved keyword.  This helper
909/// normalises the column name so the two can be compared correctly during
910/// schema migration.
911fn strip_identifier_quotes(ident: &str) -> &str {
912    let bytes = ident.as_bytes();
913    if bytes.len() >= 2 {
914        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
915        if (first == b'"' && last == b'"')
916            || (first == b'`' && last == b'`')
917            || (first == b'[' && last == b']')
918        {
919            return &ident[1..ident.len() - 1];
920        }
921    }
922    ident
923}
924
925fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
926    values.iter().map(bind_sqlite_value).collect()
927}
928
929fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
930    match value {
931        Value::Null => Ok(SqliteValue::Null),
932        Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
933        Value::I64(v) => Ok(SqliteValue::Integer(*v)),
934        Value::U64(v) => i64::try_from(*v)
935            .map(SqliteValue::Integer)
936            .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
937        Value::F64(v) => Ok(SqliteValue::Real(*v)),
938        // Bind the canonical numeric spelling. SQLite NUMERIC affinity keeps
939        // predicates and aggregates numeric; an application-only text prefix
940        // makes range comparisons silently return the wrong result.
941        Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
942        Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
943        Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
944        Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
945        Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
946        Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
947        Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
948        Value::TypedNull(_) => Ok(SqliteValue::Null),
949    }
950}
951
952#[derive(Debug, Clone)]
953struct ColumnInfo {
954    name: String,
955    decode_kind: SqliteDecodeKind,
956}
957
958#[derive(Debug, Clone, Copy, PartialEq, Eq)]
959enum SqliteDecodeKind {
960    Infer,
961    Bool,
962    Decimal,
963    Json,
964    Date,
965    Timestamp,
966    Text,
967}
968
969#[derive(Debug)]
970struct ColumnLayout {
971    columns: Arc<[ColumnInfo]>,
972    names: Arc<[String]>,
973}
974
975fn cached_column_layout(
976    cache: &Mutex<HashMap<String, Arc<ColumnLayout>>>,
977    sql: &str,
978    statement: &rusqlite::Statement<'_>,
979) -> Arc<ColumnLayout> {
980    if let Ok(cache) = cache.lock()
981        && let Some(layout) = cache.get(sql)
982    {
983        return layout.clone();
984    }
985
986    let columns: Arc<[ColumnInfo]> = statement_columns(statement).into();
987    let names = columns
988        .iter()
989        .map(|column| column.name.clone())
990        .collect::<Vec<_>>()
991        .into();
992    let layout = Arc::new(ColumnLayout { columns, names });
993    if let Ok(mut cache) = cache.lock() {
994        if cache.len() >= DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY {
995            cache.clear();
996        }
997        cache.insert(sql.to_owned(), layout.clone());
998    }
999    layout
1000}
1001
1002fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
1003    statement
1004        .columns()
1005        .into_iter()
1006        .map(|column| ColumnInfo {
1007            name: column.name().to_owned(),
1008            decode_kind: sqlite_decode_kind(column.decl_type()),
1009        })
1010        .collect()
1011}
1012
1013fn sqlite_decode_kind(decl_type: Option<&str>) -> SqliteDecodeKind {
1014    let Some(decl_type) = decl_type else {
1015        return SqliteDecodeKind::Infer;
1016    };
1017    let base = decl_type.split('(').next().unwrap_or(decl_type).trim();
1018    if base.eq_ignore_ascii_case("BOOLEAN") || base.eq_ignore_ascii_case("BOOL") {
1019        SqliteDecodeKind::Bool
1020    } else if base.eq_ignore_ascii_case("NUMERIC") || base.eq_ignore_ascii_case("DECIMAL") {
1021        SqliteDecodeKind::Decimal
1022    } else if base.eq_ignore_ascii_case("JSON") {
1023        SqliteDecodeKind::Json
1024    } else if base.eq_ignore_ascii_case("DATE") {
1025        SqliteDecodeKind::Date
1026    } else if base.eq_ignore_ascii_case("TIMESTAMP") || base.eq_ignore_ascii_case("DATETIME") {
1027        SqliteDecodeKind::Timestamp
1028    } else if ["TEXT", "VARCHAR", "CHAR", "CLOB"]
1029        .iter()
1030        .any(|v| base.eq_ignore_ascii_case(v))
1031    {
1032        SqliteDecodeKind::Text
1033    } else {
1034        SqliteDecodeKind::Infer
1035    }
1036}
1037
1038fn decode_sqlite_values(
1039    row: &Row<'_>,
1040    columns: &[ColumnInfo],
1041) -> Result<Vec<Value>, MutationExecutorError> {
1042    let mut values = Vec::with_capacity(columns.len());
1043    for (index, column) in columns.iter().enumerate() {
1044        let value_ref = row.get_ref(index)?;
1045        let value = match value_ref {
1046            ValueRef::Null => Value::Null,
1047            ValueRef::Integer(value) => decode_sqlite_integer(value, column),
1048            ValueRef::Real(value) => Value::F64(value),
1049            ValueRef::Text(value) => decode_sqlite_text(value, column)?,
1050            ValueRef::Blob(_) => {
1051                return Err(MutationExecutorError::UnsupportedColumnType(
1052                    "BLOB".to_owned(),
1053                ));
1054            }
1055        };
1056        values.push(value);
1057    }
1058    Ok(values)
1059}
1060
1061fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
1062    match column.decode_kind {
1063        SqliteDecodeKind::Bool => Value::Bool(value != 0),
1064        _ => Value::I64(value),
1065    }
1066}
1067
1068fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
1069    let value = std::str::from_utf8(value)
1070        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
1071    match column.decode_kind {
1072        SqliteDecodeKind::Decimal => Decimal::from_str(value)
1073            .map(Value::Decimal)
1074            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
1075        SqliteDecodeKind::Json => serde_json::from_str(value).map(Value::Json).map_err(|err| {
1076            MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
1077        }),
1078        SqliteDecodeKind::Date => NaiveDate::parse_from_str(value, "%Y-%m-%d")
1079            .map(Value::Date)
1080            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
1081        SqliteDecodeKind::Timestamp => parse_sqlite_timestamp(value),
1082        SqliteDecodeKind::Text | SqliteDecodeKind::Bool => Ok(Value::Text(value.to_owned())),
1083        SqliteDecodeKind::Infer => infer_sqlite_text(value),
1084    }
1085}
1086
1087fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
1088    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1089        return Ok(Value::Date(date));
1090    }
1091    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1092        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1093            timestamp.timestamp_millis(),
1094        )));
1095    }
1096    if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
1097        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1098            timestamp.and_utc().timestamp_millis(),
1099        )));
1100    }
1101    Ok(Value::Text(value.to_owned()))
1102}
1103
1104fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
1105    if let Some(timestamp) = parse_fixed_sqlite_timestamp(value) {
1106        return Ok(Value::Timestamp(teaql_core::time::Timestamp(timestamp)));
1107    }
1108    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1109        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1110            timestamp.timestamp_millis(),
1111        )));
1112    }
1113    if let Ok(timestamp) = DateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f%#z") {
1114        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1115            timestamp.timestamp_millis(),
1116        )));
1117    }
1118    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1119        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1120            date.and_hms_opt(0, 0, 0)
1121                .unwrap_or_default()
1122                .and_utc()
1123                .timestamp_millis(),
1124        )));
1125    }
1126    NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
1127        .map(|timestamp| {
1128            Value::Timestamp(teaql_core::time::Timestamp(
1129                timestamp.and_utc().timestamp_millis(),
1130            ))
1131        })
1132        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
1133}
1134
1135fn parse_fixed_sqlite_timestamp(value: &str) -> Option<i64> {
1136    let bytes = value.as_bytes();
1137    if bytes.len() < 19
1138        || bytes.get(4) != Some(&b'-')
1139        || bytes.get(7) != Some(&b'-')
1140        || !matches!(bytes.get(10), Some(b' ') | Some(b'T'))
1141        || bytes.get(13) != Some(&b':')
1142        || bytes.get(16) != Some(&b':')
1143    {
1144        return None;
1145    }
1146    let digits = |start: usize, len: usize| -> Option<u32> {
1147        bytes
1148            .get(start..start + len)?
1149            .iter()
1150            .try_fold(0_u32, |value, byte| {
1151                byte.is_ascii_digit()
1152                    .then_some(value * 10 + u32::from(*byte - b'0'))
1153            })
1154    };
1155    let date = NaiveDate::from_ymd_opt(
1156        i32::try_from(digits(0, 4)?).ok()?,
1157        digits(5, 2)?,
1158        digits(8, 2)?,
1159    )?;
1160    let hour = digits(11, 2)?;
1161    let minute = digits(14, 2)?;
1162    let second = digits(17, 2)?;
1163    let mut cursor = 19;
1164    let mut nanos = 0_u32;
1165    if bytes.get(cursor) == Some(&b'.') {
1166        cursor += 1;
1167        let fraction_start = cursor;
1168        while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
1169            if cursor - fraction_start < 9 {
1170                nanos = nanos * 10 + u32::from(bytes[cursor] - b'0');
1171            }
1172            cursor += 1;
1173        }
1174        let kept = (cursor - fraction_start).min(9);
1175        if kept == 0 {
1176            return None;
1177        }
1178        nanos *= 10_u32.pow(u32::try_from(9 - kept).ok()?);
1179    }
1180    let datetime = date.and_hms_nano_opt(hour, minute, second, nanos)?;
1181    let offset_seconds = match bytes.get(cursor..) {
1182        Some([]) | Some([b'Z']) | Some([b'z']) => 0,
1183        Some([sign @ (b'+' | b'-'), hour_1, hour_2]) => {
1184            signed_offset(*sign, [*hour_1, *hour_2], *b"00")?
1185        }
1186        Some([sign @ (b'+' | b'-'), hour_1, hour_2, minute_1, minute_2]) => {
1187            signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?
1188        }
1189        Some(
1190            [
1191                sign @ (b'+' | b'-'),
1192                hour_1,
1193                hour_2,
1194                b':',
1195                minute_1,
1196                minute_2,
1197            ],
1198        ) => signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?,
1199        _ => return None,
1200    };
1201    FixedOffset::east_opt(offset_seconds)?
1202        .from_local_datetime(&datetime)
1203        .single()
1204        .map(|timestamp| timestamp.timestamp_millis())
1205}
1206
1207fn signed_offset(sign: u8, hours: [u8; 2], minutes: [u8; 2]) -> Option<i32> {
1208    let pair = |digits: [u8; 2]| {
1209        digits
1210            .iter()
1211            .all(u8::is_ascii_digit)
1212            .then_some(i32::from(digits[0] - b'0') * 10 + i32::from(digits[1] - b'0'))
1213    };
1214    let hours = pair(hours)?;
1215    let minutes = pair(minutes)?;
1216    if hours > 23 || minutes > 59 {
1217        return None;
1218    }
1219    let seconds = hours * 3600 + minutes * 60;
1220    Some(if sign == b'-' { -seconds } else { seconds })
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::*;
1226    use futures_util::StreamExt;
1227    use teaql_core::{DeleteCommand, Entity, Record, RecoverCommand, TeaqlEntity as _};
1228    use teaql_macros::{TeaqlEntity, teaql_entity};
1229    use teaql_runtime::InMemoryMetadataStore;
1230
1231    #[teaql_entity]
1232    #[derive(Debug, TeaqlEntity)]
1233    #[teaql(entity = "TransactionSchool", table = "transaction_school")]
1234    struct TransactionSchool {
1235        #[teaql(id)]
1236        id: u64,
1237        #[teaql(version)]
1238        version: i64,
1239        name: String,
1240    }
1241
1242    impl TransactionSchool {
1243        fn new(id: u64, name: &str) -> (Self, teaql_runtime::EntityRuntimeState) {
1244            let state = teaql_runtime::EntityRuntimeState::default();
1245            let key = teaql_runtime::EntityKey::new("TransactionSchool", id);
1246            state.mark_as_new(key.clone());
1247            state.set(key.clone(), "id", id);
1248            state.set(key, "name", name);
1249            (
1250                Self {
1251                    id,
1252                    version: 0,
1253                    name: name.to_owned(),
1254                    __teaql_runtime_state: state.clone(),
1255                },
1256                state,
1257            )
1258        }
1259    }
1260
1261    #[test]
1262    fn ensure_schema_registers_soundex_idempotently() {
1263        let executor =
1264            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1265        executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1266        executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1267        let connection = executor.connection();
1268        let guard = connection.lock().unwrap();
1269        let encoded: String = guard
1270            .query_row("SELECT soundex('Robert')", [], |row| row.get(0))
1271            .unwrap();
1272        let matches: i64 = guard
1273            .query_row("SELECT soundex('Robert') = soundex('Rupert')", [], |row| {
1274                row.get(0)
1275            })
1276            .unwrap();
1277        let empty: String = guard
1278            .query_row("SELECT soundex(NULL)", [], |row| row.get(0))
1279            .unwrap();
1280        assert_eq!(encoded, "R163");
1281        assert_eq!(matches, 1);
1282        assert_eq!(empty, "?000");
1283    }
1284
1285    #[test]
1286    fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
1287        let connection = Connection::open_in_memory().unwrap();
1288        connection
1289            .execute_batch(
1290                "CREATE TABLE stream_fixture(id INTEGER);\
1291                 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
1292            )
1293            .unwrap();
1294        let executor = SqliteMutationExecutor::from_connection(connection);
1295        let query = CompiledQuery {
1296            sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
1297            params: vec![],
1298            comment: None,
1299        };
1300        let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
1301        let sizes = futures_executor::block_on(async {
1302            let mut result = Vec::new();
1303            while let Some(chunk) = stream.next().await {
1304                result.push(chunk.unwrap().rows.len());
1305            }
1306            result
1307        });
1308        assert_eq!(sizes, vec![2, 2, 1]);
1309
1310        let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
1311        assert_eq!(
1312            futures_executor::block_on(early.next())
1313                .unwrap()
1314                .unwrap()
1315                .rows
1316                .len(),
1317            2
1318        );
1319        drop(early);
1320        let count: i64 = executor
1321            .connection()
1322            .lock()
1323            .unwrap()
1324            .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
1325            .unwrap();
1326        assert_eq!(count, 5);
1327    }
1328
1329    #[test]
1330    fn decimal_bind_is_numeric_and_comparable() {
1331        let value =
1332            bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
1333        assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
1334        let connection = Connection::open_in_memory().unwrap();
1335        let matches: i64 = connection
1336            .query_row(
1337                "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
1338                [value],
1339                |row| row.get(0),
1340            )
1341            .unwrap();
1342        assert_eq!(matches, 1);
1343    }
1344
1345    #[test]
1346    fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
1347        let connection = Connection::open_in_memory().unwrap();
1348        connection
1349            .execute_batch(
1350                "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
1351            )
1352            .unwrap();
1353        let query = CompiledQuery {
1354            sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
1355            params: vec![
1356                Value::I64(1),
1357                Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
1358                Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
1359            ],
1360            comment: None,
1361        };
1362        let values = bind_values(&query.params).unwrap();
1363        connection
1364            .execute(&query.sql, rusqlite::params_from_iter(values))
1365            .unwrap();
1366        connection
1367            .execute(
1368                &query
1369                    .debug_sql(teaql_sql::DatabaseKind::Sqlite)
1370                    .replace("VALUES (1,", "VALUES (2,"),
1371                [],
1372            )
1373            .unwrap();
1374
1375        let equal_count: i64 = connection.query_row(
1376            "SELECT count(*) FROM temporal_fixture a JOIN temporal_fixture b ON a.d=b.d AND a.t=b.t WHERE a.id=1 AND b.id=2",
1377            [], |row| row.get(0),
1378        ).unwrap();
1379        let storage_type: String = connection
1380            .query_row(
1381                "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
1382                [],
1383                |row| row.get(0),
1384            )
1385            .unwrap();
1386        assert_eq!(equal_count, 1);
1387        assert_eq!(storage_type, "integer");
1388    }
1389
1390    fn entity() -> EntityDescriptor {
1391        EntityDescriptor::new("Order")
1392            .table_name("orders")
1393            .property(
1394                PropertyDescriptor::new("id", DataType::U64)
1395                    .column_name("id")
1396                    .id()
1397                    .not_null(),
1398            )
1399            .property(
1400                PropertyDescriptor::new("version", DataType::I64)
1401                    .column_name("version")
1402                    .version()
1403                    .not_null(),
1404            )
1405            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1406    }
1407
1408    #[test]
1409    fn user_context_transaction_scope_commits_and_rolls_back_on_one_connection() {
1410        let executor = SqliteMutationExecutor::from_connection(
1411            Connection::open_in_memory().expect("open transaction fixture"),
1412        );
1413        executor
1414            .connection()
1415            .lock()
1416            .expect("lock transaction fixture")
1417            .execute_batch(
1418                "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, name TEXT)",
1419            )
1420            .expect("create transaction fixture");
1421
1422        let metadata = InMemoryMetadataStore::new().with_entity(entity());
1423        let data_service = teaql_sql::SqlDataServiceExecutor::new(
1424            SqliteDialect,
1425            executor.clone(),
1426            metadata.clone(),
1427        );
1428        let mut context = UserContext::new().with_metadata(metadata);
1429        context.register_executor(data_service);
1430
1431        type Executor = teaql_sql::SqlDataServiceExecutor<
1432            SqliteDialect,
1433            SqliteMutationExecutor,
1434            InMemoryMetadataStore,
1435        >;
1436
1437        futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1438            Box::pin(async move {
1439                scope
1440                    .mutate(teaql_data_service::MutationRequest::Insert(
1441                        teaql_core::InsertCommand::new("Order")
1442                            .value("id", 1_u64)
1443                            .value("version", 1_i64)
1444                            .value("name", "first"),
1445                    ))
1446                    .await?;
1447                scope
1448                    .mutate(teaql_data_service::MutationRequest::Insert(
1449                        teaql_core::InsertCommand::new("Order")
1450                            .value("id", 2_u64)
1451                            .value("version", 1_i64)
1452                            .value("name", "second"),
1453                    ))
1454                    .await?;
1455                Ok(())
1456            })
1457        }))
1458        .expect("commit transaction scope");
1459
1460        let failed =
1461            futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1462                Box::pin(async move {
1463                    scope
1464                        .mutate(teaql_data_service::MutationRequest::Insert(
1465                            teaql_core::InsertCommand::new("Order")
1466                                .value("id", 3_u64)
1467                                .value("version", 1_i64)
1468                                .value("name", "must roll back"),
1469                        ))
1470                        .await?;
1471                    scope
1472                        .mutate(teaql_data_service::MutationRequest::Insert(
1473                            teaql_core::InsertCommand::new("Order")
1474                                .value("id", 1_u64)
1475                                .value("version", 1_i64)
1476                                .value("name", "duplicate"),
1477                        ))
1478                        .await?;
1479                    Ok(())
1480                })
1481            }));
1482        assert!(failed.is_err(), "duplicate key must fail the scope");
1483
1484        let connection = executor.connection();
1485        let guard = connection.lock().expect("lock committed fixture");
1486        let ids = guard
1487            .prepare("SELECT id FROM orders ORDER BY id")
1488            .expect("prepare committed ids")
1489            .query_map([], |row| row.get::<_, i64>(0))
1490            .expect("query committed ids")
1491            .collect::<Result<Vec<_>, _>>()
1492            .expect("read committed ids");
1493        assert_eq!(ids, vec![1, 2]);
1494    }
1495
1496    #[test]
1497    fn typed_audited_saves_share_commit_and_rollback_boundary() {
1498        let executor = SqliteMutationExecutor::from_connection(
1499            Connection::open_in_memory().expect("open audited transaction fixture"),
1500        );
1501        executor
1502            .connection()
1503            .lock()
1504            .expect("lock audited transaction fixture")
1505            .execute_batch(
1506                "CREATE TABLE transaction_school (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, name TEXT NOT NULL)",
1507            )
1508            .expect("create audited transaction fixture");
1509
1510        let metadata = InMemoryMetadataStore::new()
1511            .with_entity(TransactionSchool::entity_descriptor().clone());
1512        let data_service = teaql_sql::SqlDataServiceExecutor::new(
1513            SqliteDialect,
1514            executor.clone(),
1515            metadata.clone(),
1516        );
1517        let mut context = UserContext::new().with_metadata(metadata);
1518        context.register_executor(data_service);
1519
1520        type Executor = teaql_sql::SqlDataServiceExecutor<
1521            SqliteDialect,
1522            SqliteMutationExecutor,
1523            InMemoryMetadataStore,
1524        >;
1525
1526        futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1527            Box::pin(async move {
1528                let (first, _) = TransactionSchool::new(1, "first");
1529                let (second, _) = TransactionSchool::new(2, "second");
1530                let first = scope
1531                    .save_audited(first.audit_as("create first school"))
1532                    .await?;
1533                let second = scope
1534                    .save_audited(second.audit_as("create second school"))
1535                    .await?;
1536                assert_eq!(first.version, 1);
1537                assert_eq!(second.version, 1);
1538                Ok(())
1539            })
1540        }))
1541        .expect("commit audited transaction scope");
1542
1543        let (third, third_ledger) = TransactionSchool::new(3, "must roll back");
1544        let failed =
1545            futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1546                Box::pin(async move {
1547                    scope
1548                        .save_audited(third.audit_as("create third school"))
1549                        .await?;
1550                    let (duplicate, _) = TransactionSchool::new(1, "duplicate");
1551                    scope
1552                        .save_audited(duplicate.audit_as("force duplicate failure"))
1553                        .await?;
1554                    Ok(())
1555                })
1556            }));
1557        assert!(
1558            failed.is_err(),
1559            "duplicate audited save must fail the scope"
1560        );
1561        assert!(
1562            !third_ledger.new_keys().is_empty(),
1563            "rolled-back ledger must retain retryable mutation intent"
1564        );
1565
1566        let connection = executor.connection();
1567        let guard = connection.lock().expect("lock audited committed fixture");
1568        let rows = guard
1569            .prepare("SELECT id, name FROM transaction_school ORDER BY id")
1570            .expect("prepare audited committed rows")
1571            .query_map([], |row| {
1572                Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
1573            })
1574            .expect("query audited committed rows")
1575            .collect::<Result<Vec<_>, _>>()
1576            .expect("read audited committed rows");
1577        assert_eq!(
1578            rows,
1579            vec![(1, "first".to_owned()), (2, "second".to_owned())]
1580        );
1581    }
1582
1583    fn order_line_entity() -> EntityDescriptor {
1584        EntityDescriptor::new("OrderLine")
1585            .table_name("order_line")
1586            .property(
1587                PropertyDescriptor::new("id", DataType::U64)
1588                    .column_name("id")
1589                    .id()
1590                    .not_null(),
1591            )
1592            .property(
1593                PropertyDescriptor::new("order_id", DataType::U64)
1594                    .column_name("order_id")
1595                    .not_null(),
1596            )
1597            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1598    }
1599
1600    fn complete_query_record_entity() -> EntityDescriptor {
1601        EntityDescriptor::new("QueryRecord")
1602            .table_name("query_record_scalar")
1603            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1604            .property(PropertyDescriptor::new("required_text", DataType::Text))
1605            .property(PropertyDescriptor::new("optional_text", DataType::Text))
1606            .property(PropertyDescriptor::new("required_integer", DataType::I64))
1607            .property(PropertyDescriptor::new("optional_long", DataType::I64))
1608            .property(PropertyDescriptor::new(
1609                "required_decimal",
1610                DataType::Decimal,
1611            ))
1612            .property(PropertyDescriptor::new("required_float", DataType::F64))
1613            .property(PropertyDescriptor::new("required_double", DataType::F64))
1614            .property(PropertyDescriptor::new("required_date", DataType::Date))
1615            .property(PropertyDescriptor::new("required_time", DataType::I64))
1616            .property(PropertyDescriptor::new(
1617                "required_timestamp",
1618                DataType::Timestamp,
1619            ))
1620            .property(PropertyDescriptor::new("active", DataType::Bool))
1621            .property(PropertyDescriptor::new("reviewed", DataType::Bool))
1622            .property(
1623                PropertyDescriptor::new("version", DataType::I64)
1624                    .version()
1625                    .not_null(),
1626            )
1627    }
1628
1629    #[test]
1630    fn complete_scalar_fixture_including_nullable_boolean_executes_on_sqlite() {
1631        let executor = SqliteMutationExecutor::from_connection(
1632            Connection::open_in_memory().expect("open SQLite fixture"),
1633        );
1634        executor
1635            .connection()
1636            .lock()
1637            .expect("lock SQLite fixture")
1638            .execute_batch("CREATE TABLE query_record_scalar (\
1639                    id INTEGER PRIMARY KEY, required_text TEXT, optional_text TEXT,\
1640                    required_integer INTEGER, optional_long INTEGER, required_decimal NUMERIC,\
1641                    required_float REAL, required_double REAL, required_date DATE,\
1642                    required_time INTEGER, required_timestamp TIMESTAMP,\
1643                    active BOOLEAN, reviewed BOOLEAN, version INTEGER);\
1644                    INSERT INTO query_record_scalar VALUES \
1645                    (1,'Alpha','optional',42,42000000000,42.125,42.5,42.75,'2026-08-29',34200000,1777632600000,1,0,1),\
1646                    (2,'Beta',NULL,7,NULL,7.500,7.5,7.75,'2026-08-30',36000000,1777720400000,0,NULL,1),\
1647                    (3,'Gamma','tail',99,99000000000,99.875,99.5,99.75,'2026-08-31',37800000,1777808200000,1,1,1)")
1648            .expect("seed complete scalar fixture");
1649        let entity = complete_query_record_entity();
1650        let ids = |expr: Expr| {
1651            let query = SelectQuery::new("QueryRecord")
1652                .project("id")
1653                .filter(expr)
1654                .order_asc("id");
1655            executor
1656                .fetch_all_compact(&SqliteDialect.compile_select(&entity, &query).unwrap())
1657                .expect("execute scalar predicate")
1658                .into_iter()
1659                .map(|row| row.get("id").cloned().expect("projected id"))
1660                .collect::<Vec<_>>()
1661        };
1662        assert_eq!(ids(Expr::eq("required_text", "Alpha")), vec![Value::I64(1)]);
1663        assert_eq!(
1664            ids(Expr::ne("required_text", "Alpha")),
1665            vec![Value::I64(2), Value::I64(3)]
1666        );
1667        assert_eq!(
1668            ids(Expr::in_list(
1669                "required_text",
1670                [Value::from("Alpha"), Value::from("Gamma")]
1671            )),
1672            vec![Value::I64(1), Value::I64(3)]
1673        );
1674        assert_eq!(
1675            ids(Expr::contain("required_text", "et")),
1676            vec![Value::I64(2)]
1677        );
1678        assert_eq!(
1679            ids(Expr::between("required_integer", 40_i64, 100_i64)),
1680            vec![Value::I64(1), Value::I64(3)]
1681        );
1682        assert_eq!(
1683            ids(Expr::gt("required_decimal", Decimal::from(50))),
1684            vec![Value::I64(3)]
1685        );
1686        assert_eq!(
1687            ids(Expr::lte("required_float", 7.5_f64)),
1688            vec![Value::I64(2)]
1689        );
1690        assert_eq!(
1691            ids(Expr::gte("required_double", 99.75_f64)),
1692            vec![Value::I64(3)]
1693        );
1694        assert_eq!(
1695            ids(Expr::between(
1696                "required_date",
1697                NaiveDate::from_ymd_opt(2026, 8, 30).unwrap(),
1698                NaiveDate::from_ymd_opt(2026, 8, 31).unwrap(),
1699            )),
1700            vec![Value::I64(2), Value::I64(3)]
1701        );
1702        assert_eq!(
1703            ids(Expr::gt("required_time", 36_000_000_i64)),
1704            vec![Value::I64(3)]
1705        );
1706        assert_eq!(
1707            ids(Expr::lt(
1708                "required_timestamp",
1709                teaql_core::time::Timestamp(1_777_750_000_000)
1710            )),
1711            vec![Value::I64(1), Value::I64(2)]
1712        );
1713        assert_eq!(ids(Expr::is_null("optional_text")), vec![Value::I64(2)]);
1714        assert_eq!(
1715            ids(Expr::is_not_null("optional_long")),
1716            vec![Value::I64(1), Value::I64(3)]
1717        );
1718        assert_eq!(ids(Expr::eq("active", false)), vec![Value::I64(2)]);
1719        assert_eq!(ids(Expr::eq("reviewed", true)), vec![Value::I64(3)]);
1720        assert_eq!(ids(Expr::eq("reviewed", false)), vec![Value::I64(1)]);
1721        assert_eq!(ids(Expr::is_null("reviewed")), vec![Value::I64(2)]);
1722    }
1723
1724    #[test]
1725    fn relation_subqueries_execute_positive_and_negative_predicates_on_sqlite() {
1726        let executor = SqliteMutationExecutor::from_connection(
1727            Connection::open_in_memory().expect("open SQLite fixture"),
1728        );
1729        executor
1730            .connection()
1731            .lock()
1732            .expect("lock SQLite fixture")
1733            .execute_batch(
1734                "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT);\
1735                 CREATE TABLE order_line (id INTEGER PRIMARY KEY, order_id INTEGER, name TEXT);\
1736                 INSERT INTO orders VALUES (1, 1, 'first'), (2, 1, 'second'), (3, 1, 'third');\
1737                 INSERT INTO order_line VALUES\
1738                    (10, 1, 'priority'), (11, 1, 'ordinary'), (12, 2, 'ordinary'),\
1739                    (13, NULL, 'orphan');",
1740            )
1741            .expect("seed relation fixture");
1742
1743        let matching_lines = SelectQuery::new("OrderLine").filter(Expr::eq("name", "priority"));
1744        let positive = SelectQuery::new("Order")
1745            .project("id")
1746            .filter(Expr::in_subquery(
1747                "id",
1748                order_line_entity(),
1749                matching_lines.clone(),
1750                "order_id",
1751            ))
1752            .order_asc("id");
1753        let negative = SelectQuery::new("Order")
1754            .project("id")
1755            .filter(Expr::not_in_subquery(
1756                "id",
1757                order_line_entity(),
1758                matching_lines,
1759                "order_id",
1760            ))
1761            .order_asc("id");
1762
1763        let ids = |rows: Vec<CompactRow>| {
1764            rows.into_iter()
1765                .map(|row| row.get("id").cloned().expect("projected id"))
1766                .collect::<Vec<_>>()
1767        };
1768        let order_ids = |query: SelectQuery| {
1769            ids(executor
1770                .fetch_all_compact(&SqliteDialect.compile_select(&entity(), &query).unwrap())
1771                .expect("execute order relation predicate"))
1772        };
1773        let line_ids = |query: SelectQuery| {
1774            ids(executor
1775                .fetch_all_compact(
1776                    &SqliteDialect
1777                        .compile_select(&order_line_entity(), &query)
1778                        .unwrap(),
1779                )
1780                .expect("execute line relation predicate"))
1781        };
1782
1783        // Reverse relation: typed child matching and its negative form.
1784        assert_eq!(order_ids(positive), vec![Value::I64(1)]);
1785        assert_eq!(order_ids(negative), vec![Value::I64(2), Value::I64(3)]);
1786
1787        // Forward relation identity state keeps NULL distinct from a known FK.
1788        assert_eq!(
1789            line_ids(
1790                SelectQuery::new("OrderLine")
1791                    .project("id")
1792                    .filter(Expr::is_not_null("order_id"))
1793                    .order_asc("id")
1794            ),
1795            vec![Value::I64(10), Value::I64(11), Value::I64(12)]
1796        );
1797        assert_eq!(
1798            line_ids(
1799                SelectQuery::new("OrderLine")
1800                    .project("id")
1801                    .filter(Expr::is_null("order_id"))
1802                    .order_asc("id")
1803            ),
1804            vec![Value::I64(13)]
1805        );
1806
1807        // Forward nested matching. SQL NOT IN deliberately excludes the NULL
1808        // foreign key; callers use IsUnknown when they want orphan rows.
1809        let first_order = SelectQuery::new("Order").filter(Expr::eq("name", "first"));
1810        assert_eq!(
1811            line_ids(
1812                SelectQuery::new("OrderLine")
1813                    .project("id")
1814                    .filter(Expr::in_subquery(
1815                        "order_id",
1816                        entity(),
1817                        first_order.clone(),
1818                        "id",
1819                    ))
1820                    .order_asc("id")
1821            ),
1822            vec![Value::I64(10), Value::I64(11)]
1823        );
1824        assert_eq!(
1825            line_ids(
1826                SelectQuery::new("OrderLine")
1827                    .project("id")
1828                    .filter(Expr::not_in_subquery(
1829                        "order_id",
1830                        entity(),
1831                        first_order,
1832                        "id",
1833                    ))
1834                    .order_asc("id")
1835            ),
1836            vec![Value::I64(12)]
1837        );
1838
1839        // Reverse existence/non-existence without an additional child filter.
1840        let all_lines = SelectQuery::new("OrderLine");
1841        assert_eq!(
1842            order_ids(
1843                SelectQuery::new("Order")
1844                    .project("id")
1845                    .filter(Expr::in_subquery(
1846                        "id",
1847                        order_line_entity(),
1848                        all_lines.clone(),
1849                        "order_id",
1850                    ))
1851                    .order_asc("id")
1852            ),
1853            vec![Value::I64(1), Value::I64(2)]
1854        );
1855        assert_eq!(
1856            order_ids(
1857                SelectQuery::new("Order")
1858                    .project("id")
1859                    .filter(Expr::not_in_subquery(
1860                        "id",
1861                        order_line_entity(),
1862                        all_lines,
1863                        "order_id",
1864                    ))
1865                    .order_asc("id")
1866            ),
1867            vec![Value::I64(3)]
1868        );
1869    }
1870
1871    #[allow(dead_code)]
1872    #[derive(Debug, PartialEq, TeaqlEntity)]
1873    #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1874    struct FeatureFlagRow {
1875        #[teaql(id)]
1876        id: u64,
1877        #[teaql(version)]
1878        version: i64,
1879        enabled: bool,
1880        optional_enabled: Option<bool>,
1881    }
1882
1883    fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1884        Record::from([
1885            ("id".to_owned(), Value::U64(1)),
1886            ("version".to_owned(), Value::I64(1)),
1887            ("enabled".to_owned(), enabled),
1888            ("optional_enabled".to_owned(), optional_enabled),
1889        ])
1890    }
1891
1892    #[test]
1893    fn sqlite_dialect_compiles_mutations_and_schema() {
1894        assert!(SqliteDialect.prefers_small_parent_relation_probes());
1895        let insert = SqliteDialect
1896            .compile_insert(
1897                &entity(),
1898                &InsertCommand::new("Order")
1899                    .value("id", 1_u64)
1900                    .value("name", "A"),
1901            )
1902            .unwrap();
1903        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1904
1905        let update = SqliteDialect
1906            .compile_update(
1907                &entity(),
1908                &UpdateCommand::new("Order", 1_u64)
1909                    .expected_version(3)
1910                    .value("name", "B"),
1911            )
1912            .unwrap();
1913        assert_eq!(
1914            update.sql,
1915            "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1916        );
1917
1918        let delete = SqliteDialect
1919            .compile_delete(
1920                &entity(),
1921                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1922            )
1923            .unwrap();
1924        let recover = SqliteDialect
1925            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1926            .unwrap();
1927        assert_eq!(
1928            delete.sql,
1929            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1930        );
1931        assert_eq!(
1932            recover.sql,
1933            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1934        );
1935
1936        let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1937        assert_eq!(
1938            create,
1939            "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1940        );
1941    }
1942
1943    #[test]
1944    fn column_layout_cache_uses_parameterized_sql_not_comments() {
1945        let connection = Connection::open_in_memory().unwrap();
1946        connection
1947            .execute("CREATE TABLE sample (id INTEGER, enabled BOOLEAN)", [])
1948            .unwrap();
1949        connection
1950            .execute("INSERT INTO sample (id, enabled) VALUES (1, 1)", [])
1951            .unwrap();
1952        let executor = SqliteMutationExecutor::from_connection(connection);
1953        let mut first = CompiledQuery {
1954            sql: "SELECT id, enabled FROM sample WHERE id = ?".to_owned(),
1955            params: vec![Value::I64(1)],
1956            comment: Some("first purpose".to_owned()),
1957        };
1958        let rows = executor.fetch_all_compact(&first).unwrap();
1959        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(true)));
1960
1961        first.comment = Some("different purpose".to_owned());
1962        executor.fetch_all_compact(&first).unwrap();
1963
1964        assert_eq!(executor.column_layout_cache.lock().unwrap().len(), 1);
1965    }
1966
1967    #[test]
1968    fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1969        let executor =
1970            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1971        let entity = entity();
1972        let mut context = UserContext::new()
1973            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1974
1975        context.use_sqlite_provider(executor.clone());
1976        ensure_sqlite_schema_for(&context).unwrap();
1977
1978        let insert = SqliteDialect
1979            .compile_insert(
1980                &entity,
1981                &InsertCommand::new("Order")
1982                    .value("id", 1_u64)
1983                    .value("version", 1_i64)
1984                    .value("name", "draft"),
1985            )
1986            .unwrap();
1987        assert_eq!(executor.execute(&insert).unwrap(), 1);
1988
1989        let select = SqliteDialect
1990            .compile_select(
1991                &entity,
1992                &SelectQuery::new("Order")
1993                    .filter(Expr::eq("id", 1_u64))
1994                    .order_asc("id"),
1995            )
1996            .unwrap();
1997        let rows = executor.fetch_all_compact(&select).unwrap();
1998        assert_eq!(rows.len(), 1);
1999        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
2000        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
2001        assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
2002    }
2003
2004    #[test]
2005    fn physical_schema_never_interprets_generated_bootstrap_graphs() {
2006        let executor =
2007            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2008        let entity = entity();
2009        let mut context = UserContext::new()
2010            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
2011        context.set_root_graphs(vec![
2012            GraphNode::new("Order")
2013                .value("id", 1_u64)
2014                .value("version", 1_i64)
2015                .value("name", "module seed"),
2016        ]);
2017        context.use_sqlite_provider(executor.clone());
2018
2019        ensure_sqlite_physical_schema_for(&context).unwrap();
2020
2021        let select = SqliteDialect
2022            .compile_select(
2023                &entity,
2024                &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
2025            )
2026            .unwrap();
2027        let rows = executor.fetch_all_compact(&select).unwrap();
2028        assert!(rows.is_empty());
2029    }
2030
2031    #[test]
2032    fn public_schema_boundary_rejects_legacy_provider_owned_bootstrap_graphs() {
2033        let executor =
2034            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2035        let entity = entity();
2036        let mut context = UserContext::new()
2037            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
2038        context.set_initial_graphs(vec![
2039            GraphNode::new("Order")
2040                .value("id", 1001_u64)
2041                .value("version", 1_i64)
2042                .value("name", "red"),
2043        ]);
2044        context.use_sqlite_provider(executor.clone());
2045        let error = ensure_sqlite_schema_for(&context).unwrap_err();
2046        assert!(error.to_string().contains(
2047            "generated root/constant bootstrap must use the typed RuntimeModule callback"
2048        ));
2049        let count: i64 = executor
2050            .lock()
2051            .unwrap()
2052            .query_row("SELECT COUNT(*) FROM orders", [], |row| row.get(0))
2053            .unwrap();
2054        assert_eq!(count, 0);
2055    }
2056
2057    #[test]
2058    fn sqlite_executes_partitioned_relation_limit_per_parent() {
2059        let executor =
2060            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2061        let entity = order_line_entity();
2062        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
2063
2064        for order_id in [11_u64, 12_u64] {
2065            for index in 1_u64..=5 {
2066                let id = order_id * 100 + index;
2067                let insert = SqliteDialect
2068                    .compile_insert(
2069                        &entity,
2070                        &InsertCommand::new("OrderLine")
2071                            .value("id", id)
2072                            .value("order_id", order_id)
2073                            .value("name", format!("line-{id}")),
2074                    )
2075                    .unwrap();
2076                executor.execute(&insert).unwrap();
2077            }
2078        }
2079
2080        let query = SelectQuery::new("OrderLine")
2081            .project("id")
2082            .project("order_id")
2083            .order_desc("id")
2084            .limit(3)
2085            .partition_by("order_id");
2086        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2087        let rows = executor.fetch_all_compact(&compiled).unwrap();
2088
2089        assert_eq!(rows.len(), 6);
2090        for order_id in [11_i64, 12_i64] {
2091            let ids = rows
2092                .iter()
2093                .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
2094                .filter_map(|row| row.get("id").cloned())
2095                .collect::<Vec<_>>();
2096            assert_eq!(
2097                ids,
2098                vec![
2099                    Value::I64(order_id * 100 + 5),
2100                    Value::I64(order_id * 100 + 4),
2101                    Value::I64(order_id * 100 + 3),
2102                ]
2103            );
2104        }
2105    }
2106
2107    #[test]
2108    fn topn_005_007_window_and_probes_preserve_results_and_predicates() {
2109        futures_executor::block_on(async {
2110            #[derive(Clone)]
2111            struct FixedSchema(Arc<EntityDescriptor>);
2112
2113            impl teaql_data_service::SchemaProvider for FixedSchema {
2114                fn get_entity(&self, name: &str) -> Option<Arc<EntityDescriptor>> {
2115                    (name == self.0.name).then(|| self.0.clone())
2116                }
2117            }
2118
2119            let transport =
2120                SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2121            let entity = Arc::new(order_line_entity());
2122            transport
2123                .ensure_schema(&SqliteDialect, &[entity.as_ref()])
2124                .unwrap();
2125
2126            for order_id in [11_u64, 12_u64, 13_u64] {
2127                for index in 1_u64..=5 {
2128                    let id = order_id * 100 + index;
2129                    let name = if index == 4 { "excluded" } else { "visible" };
2130                    let insert = SqliteDialect
2131                        .compile_insert(
2132                            &entity,
2133                            &InsertCommand::new("OrderLine")
2134                                .value("id", id)
2135                                .value("order_id", order_id)
2136                                .value("name", name),
2137                        )
2138                        .unwrap();
2139                    transport.execute(&insert).unwrap();
2140                }
2141            }
2142
2143            let executor = teaql_sql::SqlDataServiceExecutor::new(
2144                SqliteDialect,
2145                transport,
2146                FixedSchema(entity),
2147            );
2148            let base = SelectQuery::new("OrderLine")
2149                .project("id")
2150                .project("order_id")
2151                .project("name")
2152                .filter(Expr::in_list("order_id", [Value::U64(11), Value::U64(12)]))
2153                .and_filter(Expr::eq("name", "visible"))
2154                .order_desc("id")
2155                .limit(3)
2156                .partition_by("order_id");
2157            let execute = |query| {
2158                teaql_data_service::QueryExecutor::query(
2159                    &executor,
2160                    teaql_data_service::QueryRequest {
2161                        query,
2162                        trace_chain: Vec::new(),
2163                        comment: Some("TOPN plan equivalence".to_owned()),
2164                        capture_debug_query: false,
2165                        capture_execution_metadata: false,
2166                    },
2167                )
2168            };
2169
2170            let probes = execute(base.clone()).await.unwrap().rows;
2171            let window = execute(base.top_n_probe_parent_threshold(0))
2172                .await
2173                .unwrap()
2174                .rows;
2175            let children_of = |rows: &[CompactRow], parent: i64| {
2176                rows.iter()
2177                    .filter(|row| row.get("order_id") == Some(&Value::I64(parent)))
2178                    .map(|row| (row.get("id").cloned(), row.get("name").cloned()))
2179                    .collect::<Vec<_>>()
2180            };
2181
2182            for parent in [11_i64, 12_i64] {
2183                assert_eq!(children_of(&probes, parent), children_of(&window, parent));
2184            }
2185            assert_eq!(
2186                children_of(&window, 11),
2187                vec![
2188                    (Some(Value::I64(1105)), Some(Value::Text("visible".into()))),
2189                    (Some(Value::I64(1103)), Some(Value::Text("visible".into()))),
2190                    (Some(Value::I64(1102)), Some(Value::Text("visible".into()))),
2191                ]
2192            );
2193            assert_eq!(
2194                children_of(&window, 12),
2195                vec![
2196                    (Some(Value::I64(1205)), Some(Value::Text("visible".into()))),
2197                    (Some(Value::I64(1203)), Some(Value::Text("visible".into()))),
2198                    (Some(Value::I64(1202)), Some(Value::Text("visible".into()))),
2199                ]
2200            );
2201            assert!(children_of(&probes, 13).is_empty());
2202            assert!(children_of(&window, 13).is_empty());
2203        });
2204    }
2205
2206    #[test]
2207    fn sqlite_boolean_new_schema_roundtrips_as_bool() {
2208        let executor =
2209            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2210        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2211        let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
2212        assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
2213        assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
2214        assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
2215
2216        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
2217        for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
2218            let insert = SqliteDialect
2219                .compile_insert(
2220                    &entity,
2221                    &InsertCommand::new("FeatureFlag")
2222                        .value("id", id)
2223                        .value("version", 1_i64)
2224                        .value("enabled", enabled)
2225                        .value("optional_enabled", optional_enabled),
2226                )
2227                .unwrap();
2228            assert_eq!(executor.execute(&insert).unwrap(), 1);
2229        }
2230
2231        let select = SqliteDialect
2232            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2233            .unwrap();
2234        let rows = executor.fetch_all_compact(&select).unwrap();
2235        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
2236        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
2237        assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
2238        assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
2239
2240        let first =
2241            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2242        let second =
2243            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap();
2244        assert!(!first.enabled);
2245        assert_eq!(first.optional_enabled, Some(true));
2246        assert!(second.enabled);
2247        assert_eq!(second.optional_enabled, Some(false));
2248    }
2249
2250    #[test]
2251    fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
2252        let executor =
2253            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2254        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2255        executor
2256            .execute(&CompiledQuery {
2257                sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
2258                    .to_owned(),
2259                params: Vec::new(),
2260                comment: None,
2261            })
2262            .unwrap();
2263
2264        let insert = SqliteDialect
2265            .compile_insert(
2266                &entity,
2267                &InsertCommand::new("FeatureFlag")
2268                    .value("id", 1_u64)
2269                    .value("version", 1_i64)
2270                    .value("enabled", true)
2271                    .value("optional_enabled", false),
2272            )
2273            .unwrap();
2274        executor.execute(&insert).unwrap();
2275        executor
2276            .execute(&CompiledQuery {
2277                sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
2278                    .to_owned(),
2279                params: vec![
2280                    Value::U64(2),
2281                    Value::I64(1),
2282                    Value::I64(2),
2283                    Value::Null,
2284                ],
2285                comment: None,
2286            })
2287            .unwrap();
2288        let select = SqliteDialect
2289            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2290            .unwrap();
2291        let rows = executor.fetch_all_compact(&select).unwrap();
2292        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
2293        assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
2294        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
2295
2296        let decoded =
2297            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2298        assert!(decoded.enabled);
2299        assert_eq!(decoded.optional_enabled, Some(false));
2300        assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
2301        let error =
2302            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap_err();
2303        assert!(error.message.contains("invalid field enabled"));
2304
2305        for (value, expected) in [
2306            (Value::I64(0), false),
2307            (Value::I64(1), true),
2308            (Value::U64(0), false),
2309            (Value::U64(1), true),
2310        ] {
2311            let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2312                teaql_core::CompactRow::from_map(feature_flag_record(value, Value::Null)),
2313            )
2314            .unwrap();
2315            assert_eq!(decoded.enabled, expected);
2316            assert_eq!(decoded.optional_enabled, None);
2317        }
2318
2319        for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
2320            let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2321                teaql_core::CompactRow::from_map(feature_flag_record(invalid, Value::Null)),
2322            )
2323            .unwrap_err();
2324            assert!(error.message.contains("invalid field enabled"));
2325        }
2326        let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2327            teaql_core::CompactRow::from_map(feature_flag_record(Value::Bool(true), Value::U64(2))),
2328        )
2329        .unwrap_err();
2330        assert!(error.message.contains("invalid field optional_enabled"));
2331    }
2332
2333    #[test]
2334    fn sqlite_executor_parses_json_only_for_json_columns() {
2335        let executor =
2336            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2337
2338        executor
2339            .execute(&CompiledQuery {
2340                sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
2341                params: Vec::new(),
2342                comment: None,
2343            })
2344            .unwrap();
2345        executor
2346            .execute(&CompiledQuery {
2347                sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
2348                params: vec![
2349                    Value::Text("{\"active\":true}".to_owned()),
2350                    Value::Json(serde_json::json!({"active": true})),
2351                ],
2352                comment: None,
2353            })
2354            .unwrap();
2355
2356        let rows = executor
2357            .fetch_all_compact(&CompiledQuery {
2358                sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
2359                params: Vec::new(),
2360                comment: None,
2361            })
2362            .unwrap();
2363
2364        assert_eq!(
2365            rows[0].get("text_payload"),
2366            Some(&Value::Text("{\"active\":true}".to_owned()))
2367        );
2368        assert_eq!(
2369            rows[0].get("json_payload"),
2370            Some(&Value::Json(serde_json::json!({"active": true})))
2371        );
2372    }
2373
2374    #[test]
2375    fn sqlite_id_space_generator_increments_ids() {
2376        let executor =
2377            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2378        let generator = SqliteIdSpaceGenerator::from_executor(executor);
2379        assert_eq!(generator.next_id("Order").unwrap(), 1);
2380        assert_eq!(generator.next_id("Order").unwrap(), 2);
2381    }
2382
2383    #[test]
2384    fn sqlite_id_space_generator_is_safe_across_connections() {
2385        let path = std::env::temp_dir().join(format!(
2386            "teaql-id-space-{}-{}.db",
2387            std::process::id(),
2388            std::time::SystemTime::now()
2389                .duration_since(std::time::UNIX_EPOCH)
2390                .unwrap()
2391                .as_nanos()
2392        ));
2393        let mut workers = Vec::new();
2394        for _ in 0..4 {
2395            let path = path.clone();
2396            workers.push(std::thread::spawn(move || {
2397                let connection = Connection::open(path).unwrap();
2398                connection
2399                    .busy_timeout(std::time::Duration::from_secs(5))
2400                    .unwrap();
2401                let generator = SqliteIdSpaceGenerator::new(connection);
2402                (0..25)
2403                    .map(|_| generator.next_id("Order").unwrap())
2404                    .collect::<Vec<_>>()
2405            }));
2406        }
2407        let mut ids = workers
2408            .into_iter()
2409            .flat_map(|worker| worker.join().unwrap())
2410            .collect::<Vec<_>>();
2411        ids.sort_unstable();
2412        assert_eq!(ids, (1..=100).collect::<Vec<_>>());
2413        let _ = std::fs::remove_file(path);
2414    }
2415
2416    #[test]
2417    fn sqlite_fetch_stream_returns_chunked_rows() {
2418        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2419            Connection::open_in_memory().unwrap(),
2420        )));
2421        let entity = entity();
2422
2423        // Create table and insert 25 rows
2424        executor
2425            .execute(&CompiledQuery {
2426                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2427                    .to_owned(),
2428                params: Vec::new(),
2429                comment: None,
2430            })
2431            .unwrap();
2432
2433        for i in 1..=25 {
2434            let insert = SqliteDialect
2435                .compile_insert(
2436                    &entity,
2437                    &InsertCommand::new("Order")
2438                        .value("id", i as u64)
2439                        .value("version", 1_i64)
2440                        .value("name", format!("order-{i}")),
2441                )
2442                .unwrap();
2443            executor.execute(&insert).unwrap();
2444        }
2445
2446        // Stream with chunk_size = 10
2447        let query = SelectQuery::new("Order")
2448            .filter(Expr::gt("version", 0_i64))
2449            .order_asc("id")
2450            .stream(10);
2451
2452        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2453
2454        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2455
2456        // 25 rows / 10 per chunk = 3 chunks
2457        assert_eq!(chunks.len(), 3);
2458        assert_eq!(chunks[0].rows.len(), 10);
2459        assert_eq!(chunks[0].chunk_index, 0);
2460        assert!(!chunks[0].is_last);
2461
2462        assert_eq!(chunks[1].rows.len(), 10);
2463        assert_eq!(chunks[1].chunk_index, 1);
2464        assert!(!chunks[1].is_last);
2465
2466        assert_eq!(chunks[2].rows.len(), 5);
2467        assert_eq!(chunks[2].chunk_index, 2);
2468        assert!(chunks[2].is_last);
2469
2470        // Verify first and last row
2471        assert_eq!(
2472            chunks[0].rows[0].get("name"),
2473            Some(&Value::Text("order-1".to_owned()))
2474        );
2475        assert_eq!(
2476            chunks[2].rows[4].get("name"),
2477            Some(&Value::Text("order-25".to_owned()))
2478        );
2479    }
2480
2481    #[test]
2482    fn sqlite_fetch_stream_handles_empty_result() {
2483        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2484            Connection::open_in_memory().unwrap(),
2485        )));
2486
2487        executor
2488            .execute(&CompiledQuery {
2489                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2490                    .to_owned(),
2491                params: Vec::new(),
2492                comment: None,
2493            })
2494            .unwrap();
2495
2496        let entity = entity();
2497        let query = SelectQuery::new("Order")
2498            .filter(Expr::gt("version", 0_i64))
2499            .stream(10);
2500
2501        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2502
2503        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2504
2505        // Empty result = 1 chunk with 0 rows, marked as last
2506        assert_eq!(chunks.len(), 1);
2507        assert_eq!(chunks[0].rows.len(), 0);
2508        assert!(chunks[0].is_last);
2509    }
2510
2511    #[test]
2512    fn sqlite_fetch_stream_exact_chunk_boundary() {
2513        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2514            Connection::open_in_memory().unwrap(),
2515        )));
2516        let entity = entity();
2517
2518        executor
2519            .execute(&CompiledQuery {
2520                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2521                    .to_owned(),
2522                params: Vec::new(),
2523                comment: None,
2524            })
2525            .unwrap();
2526
2527        // Insert exactly 20 rows
2528        for i in 1..=20 {
2529            let insert = SqliteDialect
2530                .compile_insert(
2531                    &entity,
2532                    &InsertCommand::new("Order")
2533                        .value("id", i as u64)
2534                        .value("version", 1_i64)
2535                        .value("name", format!("order-{i}")),
2536                )
2537                .unwrap();
2538            executor.execute(&insert).unwrap();
2539        }
2540
2541        let query = SelectQuery::new("Order")
2542            .filter(Expr::gt("version", 0_i64))
2543            .order_asc("id")
2544            .stream(10);
2545
2546        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2547
2548        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2549
2550        // 20 rows / 10 per chunk = 2 full chunks + 1 empty final chunk
2551        assert_eq!(chunks.len(), 3);
2552        assert_eq!(chunks[0].rows.len(), 10);
2553        assert!(!chunks[0].is_last);
2554        assert_eq!(chunks[1].rows.len(), 10);
2555        assert!(!chunks[1].is_last);
2556        assert_eq!(chunks[2].rows.len(), 0);
2557        assert!(chunks[2].is_last);
2558    }
2559
2560    #[test]
2561    fn test_parse_sqlite_timestamp() {
2562        let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
2563        assert!(matches!(ts1, Value::Timestamp(_)));
2564
2565        let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
2566        assert!(matches!(ts2, Value::Timestamp(_)));
2567
2568        let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
2569        assert!(matches!(ts3, Value::Timestamp(_)));
2570
2571        let ts4 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546+00").unwrap();
2572        assert!(matches!(ts4, Value::Timestamp(_)));
2573
2574        let ts5 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546").unwrap();
2575        assert!(matches!(ts5, Value::Timestamp(_)));
2576
2577        assert_eq!(
2578            parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+00"),
2579            Some(1_704_067_200_000)
2580        );
2581        assert_eq!(
2582            parse_fixed_sqlite_timestamp("2024-01-01T08:00:00.123+08:00"),
2583            Some(1_704_067_200_123)
2584        );
2585        assert_eq!(
2586            parse_fixed_sqlite_timestamp("2023-12-31 19:00:00-0500"),
2587            Some(1_704_067_200_000)
2588        );
2589        assert_eq!(parse_fixed_sqlite_timestamp("2024-13-01 00:00:00Z"), None);
2590        assert_eq!(parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+24"), None);
2591
2592        assert!(parse_sqlite_timestamp("invalid").is_err());
2593    }
2594
2595    #[test]
2596    fn declared_text_does_not_infer_timestamp_from_content() {
2597        for decl_type in ["TEXT", "VARCHAR(255)", "CHAR(32)", "CLOB"] {
2598            let column = ColumnInfo {
2599                name: "external_timestamp".to_owned(),
2600                decode_kind: sqlite_decode_kind(Some(decl_type)),
2601            };
2602
2603            assert_eq!(
2604                decode_sqlite_text(b"2024-01-01 00:57:55", &column).unwrap(),
2605                Value::Text("2024-01-01 00:57:55".to_owned())
2606            );
2607        }
2608    }
2609
2610    #[test]
2611    fn declared_column_types_compile_to_decode_kinds() {
2612        assert_eq!(sqlite_decode_kind(Some("BOOLEAN")), SqliteDecodeKind::Bool);
2613        assert_eq!(
2614            sqlite_decode_kind(Some("decimal(20, 4)")),
2615            SqliteDecodeKind::Decimal
2616        );
2617        assert_eq!(
2618            sqlite_decode_kind(Some(" VARCHAR(255) ")),
2619            SqliteDecodeKind::Text
2620        );
2621        assert_eq!(
2622            sqlite_decode_kind(Some("datetime")),
2623            SqliteDecodeKind::Timestamp
2624        );
2625        assert_eq!(sqlite_decode_kind(Some("custom")), SqliteDecodeKind::Infer);
2626        assert_eq!(sqlite_decode_kind(None), SqliteDecodeKind::Infer);
2627    }
2628}