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_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
599    let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
600        MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
601    })?;
602    let executor = context
603        .get_resource::<SqliteMutationExecutor>()
604        .ok_or_else(|| {
605            MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
606        })?;
607
608    let entities = context.all_entities();
609
610    // Ensure id space table exists
611    executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
612
613    // Process each entity table individually with granular events
614    for entity in &entities {
615        let field_count = entity.properties.len();
616        if !executor.table_exists(&entity.table_name)? {
617            // New table: create it
618            let sql = dialect.compile_create_table(entity)?;
619            executor.lock()?.execute(&sql, [])?;
620            let _ = context.send_event(RawAuditEvent::schema_created(
621                &entity.name,
622                &entity.table_name,
623                field_count,
624            ));
625            continue;
626        }
627        // Existing table: check for missing columns
628        let existing_columns = executor.table_columns(&entity.table_name)?;
629        let mut fields_added = 0;
630        for property in &entity.properties {
631            let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
632            if existing_columns.contains(&bare_column) {
633                continue;
634            }
635            let sql = dialect.compile_add_column(entity, property)?;
636            executor.lock()?.execute(&sql, [])?;
637            let _ = context.send_event(RawAuditEvent::field_added(
638                &entity.name,
639                &entity.table_name,
640                &property.column_name,
641            ));
642            fields_added += 1;
643        }
644        let _ = context.send_event(RawAuditEvent::schema_verified(
645            &entity.name,
646            &entity.table_name,
647            field_count,
648        ));
649        let _ = fields_added; // used above for FieldAdded events
650    }
651
652    // Constant graphs are reconciled so model changes are propagated.
653    let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
654    let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); // (inserted, updated)
655    for graph in context.initial_graphs() {
656        let entity = context.entity(&graph.entity).ok_or_else(|| {
657            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
658        })?;
659        let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
660        if let Some(current) = initial_graph_row_sqlite(executor, dialect, entity, graph)? {
661            if let Some(query) = compile_initial_graph_update(dialect, entity, graph, &current)? {
662                executor.execute(&query)?;
663                counts.1 += 1;
664            }
665            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
666                id_generator.ensure_floor(&graph.entity, id)?;
667            }
668            continue;
669        }
670        let query = compile_initial_graph_insert(dialect, entity, graph)?;
671        executor.execute(&query)?;
672        counts.0 += 1; // inserted
673        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
674            id_generator.ensure_floor(&graph.entity, id)?;
675        }
676    }
677
678    // Roots are create-if-absent. Once present, application-owned values win.
679    for graph in context.root_graphs() {
680        let entity = context.entity(&graph.entity).ok_or_else(|| {
681            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
682        })?;
683        if initial_graph_row_sqlite(executor, dialect, entity, graph)?.is_some() {
684            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
685                id_generator.ensure_floor(&graph.entity, id)?;
686            }
687            continue;
688        }
689        let query = compile_initial_graph_insert(dialect, entity, graph)?;
690        executor.execute(&query)?;
691        seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
692        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
693            id_generator.ensure_floor(&graph.entity, id)?;
694        }
695    }
696
697    // Fire DataSeeded events per entity type
698    for (entity_name, (inserted, updated)) in &seed_counts {
699        let entity = context.entity(entity_name).ok_or_else(|| {
700            MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
701        })?;
702        let _ = context.send_event(RawAuditEvent::data_seeded(
703            entity_name,
704            &entity.table_name,
705            *inserted,
706            *updated,
707        ));
708    }
709
710    executor.clear_query_caches();
711    Ok(())
712}
713
714#[derive(Debug, Default, Clone, Copy)]
715pub struct SqliteSchemaProvider;
716
717impl SchemaProvider for SqliteSchemaProvider {
718    fn ensure_schema<'a>(
719        &'a self,
720        context: &'a UserContext,
721        _invocation: &'a teaql_runtime::SchemaInvocation,
722    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
723        Box::pin(async move {
724            ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
725        })
726    }
727}
728
729pub trait SqliteProviderExt {
730    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
731}
732
733impl SqliteProviderExt for UserContext {
734    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
735        self.insert_resource(SqliteDialect);
736        self.insert_resource(executor);
737        self.set_schema_provider(SqliteSchemaProvider);
738        self
739    }
740}
741
742#[derive(Clone)]
743pub struct SqliteIdSpaceGenerator {
744    executor: SqliteMutationExecutor,
745    table_name: String,
746}
747
748impl SqliteIdSpaceGenerator {
749    pub fn new(connection: Connection) -> Self {
750        Self::from_executor(SqliteMutationExecutor::from_connection(connection))
751    }
752
753    pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
754        Self {
755            executor,
756            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
757        }
758    }
759
760    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
761        self.table_name = table_name.into();
762        self
763    }
764
765    pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
766        self.executor.ensure_id_space_table(&self.table_name)
767    }
768
769    pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
770        let entity = canonical_id_space_entity(entity);
771        let entity = entity.as_str();
772        self.ensure_table()?;
773        let table = quote_ident(&self.table_name);
774        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
775        let insert_sql = format!("INSERT INTO {table} (type_name, current_level) VALUES (?, 1)");
776        let update_sql = format!(
777            "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
778        );
779        for attempt in 1..=100 {
780            let connection = self.executor.lock()?;
781            let current = connection
782                .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
783                .optional()?;
784            if let Some(current) = current {
785                let next = current.checked_add(1).ok_or_else(|| {
786                    MutationExecutorError::Bind(format!(
787                        "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
788                    ))
789                })?;
790                if connection.execute(&update_sql, params![next, entity, current])? == 1 {
791                    return u64::try_from(next).map_err(|_| {
792                        MutationExecutorError::Bind(format!(
793                            "generated id {next} cannot be represented as u64"
794                        ))
795                    });
796                }
797            } else {
798                match connection.execute(&insert_sql, params![entity]) {
799                    Ok(1) => return Ok(1),
800                    Ok(changed) => {
801                        return Err(MutationExecutorError::Bind(format!(
802                            "ID space insert for {entity} changed {changed} rows"
803                        )));
804                    }
805                    Err(error)
806                        if error.sqlite_error_code()
807                            == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
808                    Err(error) => return Err(error.into()),
809                }
810            }
811        }
812        Err(MutationExecutorError::Bind(format!(
813            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
814        )))
815    }
816
817    pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
818        let entity = canonical_id_space_entity(entity);
819        let entity = entity.as_str();
820        self.ensure_table()?;
821        let floor = i64::try_from(floor).map_err(|_| {
822            MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
823        })?;
824        let table = quote_ident(&self.table_name);
825        for _ in 1..=100 {
826            let connection = self.executor.lock()?;
827            let current = connection
828                .query_row(
829                    &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
830                    [entity],
831                    |row| row.get::<_, i64>(0),
832                )
833                .optional()?;
834            match current {
835                Some(current) if current >= floor => return Ok(()),
836                Some(current) => {
837                    if connection.execute(
838                        &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
839                        params![floor, entity, current],
840                    )? == 1 { return Ok(()); }
841                }
842                None => match connection.execute(
843                    &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
844                    params![entity, floor],
845                ) {
846                    Ok(1) => return Ok(()),
847                    Ok(_) => {}
848                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
849                    Err(error) => return Err(error.into()),
850                },
851            }
852        }
853        Err(MutationExecutorError::Bind(format!(
854            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
855        )))
856    }
857}
858
859impl InternalIdGenerator for SqliteIdSpaceGenerator {
860    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
861        self.next_id(entity)
862            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
863    }
864}
865
866fn quote_ident(ident: &str) -> String {
867    quote_identifier_if_needed(ident, '"')
868}
869
870/// Strip wrapping identifier quotes from a SQL identifier.
871///
872/// SQLite `PRAGMA table_info` returns bare column names (e.g. `description`),
873/// but generated `PropertyDescriptor::column_name` may carry quotes
874/// (e.g. `"description"`) when the name is a reserved keyword.  This helper
875/// normalises the column name so the two can be compared correctly during
876/// schema migration.
877fn strip_identifier_quotes(ident: &str) -> &str {
878    let bytes = ident.as_bytes();
879    if bytes.len() >= 2 {
880        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
881        if (first == b'"' && last == b'"')
882            || (first == b'`' && last == b'`')
883            || (first == b'[' && last == b']')
884        {
885            return &ident[1..ident.len() - 1];
886        }
887    }
888    ident
889}
890
891fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
892    values.iter().map(bind_sqlite_value).collect()
893}
894
895fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
896    match value {
897        Value::Null => Ok(SqliteValue::Null),
898        Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
899        Value::I64(v) => Ok(SqliteValue::Integer(*v)),
900        Value::U64(v) => i64::try_from(*v)
901            .map(SqliteValue::Integer)
902            .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
903        Value::F64(v) => Ok(SqliteValue::Real(*v)),
904        // Bind the canonical numeric spelling. SQLite NUMERIC affinity keeps
905        // predicates and aggregates numeric; an application-only text prefix
906        // makes range comparisons silently return the wrong result.
907        Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
908        Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
909        Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
910        Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
911        Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
912        Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
913        Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
914        Value::TypedNull(_) => Ok(SqliteValue::Null),
915    }
916}
917
918#[derive(Debug, Clone)]
919struct ColumnInfo {
920    name: String,
921    decode_kind: SqliteDecodeKind,
922}
923
924#[derive(Debug, Clone, Copy, PartialEq, Eq)]
925enum SqliteDecodeKind {
926    Infer,
927    Bool,
928    Decimal,
929    Json,
930    Date,
931    Timestamp,
932    Text,
933}
934
935#[derive(Debug)]
936struct ColumnLayout {
937    columns: Arc<[ColumnInfo]>,
938    names: Arc<[String]>,
939}
940
941fn cached_column_layout(
942    cache: &Mutex<HashMap<String, Arc<ColumnLayout>>>,
943    sql: &str,
944    statement: &rusqlite::Statement<'_>,
945) -> Arc<ColumnLayout> {
946    if let Ok(cache) = cache.lock()
947        && let Some(layout) = cache.get(sql)
948    {
949        return layout.clone();
950    }
951
952    let columns: Arc<[ColumnInfo]> = statement_columns(statement).into();
953    let names = columns
954        .iter()
955        .map(|column| column.name.clone())
956        .collect::<Vec<_>>()
957        .into();
958    let layout = Arc::new(ColumnLayout { columns, names });
959    if let Ok(mut cache) = cache.lock() {
960        if cache.len() >= DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY {
961            cache.clear();
962        }
963        cache.insert(sql.to_owned(), layout.clone());
964    }
965    layout
966}
967
968fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
969    statement
970        .columns()
971        .into_iter()
972        .map(|column| ColumnInfo {
973            name: column.name().to_owned(),
974            decode_kind: sqlite_decode_kind(column.decl_type()),
975        })
976        .collect()
977}
978
979fn sqlite_decode_kind(decl_type: Option<&str>) -> SqliteDecodeKind {
980    let Some(decl_type) = decl_type else {
981        return SqliteDecodeKind::Infer;
982    };
983    let base = decl_type.split('(').next().unwrap_or(decl_type).trim();
984    if base.eq_ignore_ascii_case("BOOLEAN") || base.eq_ignore_ascii_case("BOOL") {
985        SqliteDecodeKind::Bool
986    } else if base.eq_ignore_ascii_case("NUMERIC") || base.eq_ignore_ascii_case("DECIMAL") {
987        SqliteDecodeKind::Decimal
988    } else if base.eq_ignore_ascii_case("JSON") {
989        SqliteDecodeKind::Json
990    } else if base.eq_ignore_ascii_case("DATE") {
991        SqliteDecodeKind::Date
992    } else if base.eq_ignore_ascii_case("TIMESTAMP") || base.eq_ignore_ascii_case("DATETIME") {
993        SqliteDecodeKind::Timestamp
994    } else if ["TEXT", "VARCHAR", "CHAR", "CLOB"]
995        .iter()
996        .any(|v| base.eq_ignore_ascii_case(v))
997    {
998        SqliteDecodeKind::Text
999    } else {
1000        SqliteDecodeKind::Infer
1001    }
1002}
1003
1004fn decode_sqlite_values(
1005    row: &Row<'_>,
1006    columns: &[ColumnInfo],
1007) -> Result<Vec<Value>, MutationExecutorError> {
1008    let mut values = Vec::with_capacity(columns.len());
1009    for (index, column) in columns.iter().enumerate() {
1010        let value_ref = row.get_ref(index)?;
1011        let value = match value_ref {
1012            ValueRef::Null => Value::Null,
1013            ValueRef::Integer(value) => decode_sqlite_integer(value, column),
1014            ValueRef::Real(value) => Value::F64(value),
1015            ValueRef::Text(value) => decode_sqlite_text(value, column)?,
1016            ValueRef::Blob(_) => {
1017                return Err(MutationExecutorError::UnsupportedColumnType(
1018                    "BLOB".to_owned(),
1019                ));
1020            }
1021        };
1022        values.push(value);
1023    }
1024    Ok(values)
1025}
1026
1027fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
1028    match column.decode_kind {
1029        SqliteDecodeKind::Bool => Value::Bool(value != 0),
1030        _ => Value::I64(value),
1031    }
1032}
1033
1034fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
1035    let value = std::str::from_utf8(value)
1036        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
1037    match column.decode_kind {
1038        SqliteDecodeKind::Decimal => Decimal::from_str(value)
1039            .map(Value::Decimal)
1040            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
1041        SqliteDecodeKind::Json => serde_json::from_str(value).map(Value::Json).map_err(|err| {
1042            MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
1043        }),
1044        SqliteDecodeKind::Date => NaiveDate::parse_from_str(value, "%Y-%m-%d")
1045            .map(Value::Date)
1046            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
1047        SqliteDecodeKind::Timestamp => parse_sqlite_timestamp(value),
1048        SqliteDecodeKind::Text | SqliteDecodeKind::Bool => Ok(Value::Text(value.to_owned())),
1049        SqliteDecodeKind::Infer => infer_sqlite_text(value),
1050    }
1051}
1052
1053fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
1054    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1055        return Ok(Value::Date(date));
1056    }
1057    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1058        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1059            timestamp.timestamp_millis(),
1060        )));
1061    }
1062    if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
1063        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1064            timestamp.and_utc().timestamp_millis(),
1065        )));
1066    }
1067    Ok(Value::Text(value.to_owned()))
1068}
1069
1070fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
1071    if let Some(timestamp) = parse_fixed_sqlite_timestamp(value) {
1072        return Ok(Value::Timestamp(teaql_core::time::Timestamp(timestamp)));
1073    }
1074    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1075        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1076            timestamp.timestamp_millis(),
1077        )));
1078    }
1079    if let Ok(timestamp) = DateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f%#z") {
1080        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1081            timestamp.timestamp_millis(),
1082        )));
1083    }
1084    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1085        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1086            date.and_hms_opt(0, 0, 0)
1087                .unwrap_or_default()
1088                .and_utc()
1089                .timestamp_millis(),
1090        )));
1091    }
1092    NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
1093        .map(|timestamp| {
1094            Value::Timestamp(teaql_core::time::Timestamp(
1095                timestamp.and_utc().timestamp_millis(),
1096            ))
1097        })
1098        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
1099}
1100
1101fn parse_fixed_sqlite_timestamp(value: &str) -> Option<i64> {
1102    let bytes = value.as_bytes();
1103    if bytes.len() < 19
1104        || bytes.get(4) != Some(&b'-')
1105        || bytes.get(7) != Some(&b'-')
1106        || !matches!(bytes.get(10), Some(b' ') | Some(b'T'))
1107        || bytes.get(13) != Some(&b':')
1108        || bytes.get(16) != Some(&b':')
1109    {
1110        return None;
1111    }
1112    let digits = |start: usize, len: usize| -> Option<u32> {
1113        bytes
1114            .get(start..start + len)?
1115            .iter()
1116            .try_fold(0_u32, |value, byte| {
1117                byte.is_ascii_digit()
1118                    .then_some(value * 10 + u32::from(*byte - b'0'))
1119            })
1120    };
1121    let date = NaiveDate::from_ymd_opt(
1122        i32::try_from(digits(0, 4)?).ok()?,
1123        digits(5, 2)?,
1124        digits(8, 2)?,
1125    )?;
1126    let hour = digits(11, 2)?;
1127    let minute = digits(14, 2)?;
1128    let second = digits(17, 2)?;
1129    let mut cursor = 19;
1130    let mut nanos = 0_u32;
1131    if bytes.get(cursor) == Some(&b'.') {
1132        cursor += 1;
1133        let fraction_start = cursor;
1134        while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
1135            if cursor - fraction_start < 9 {
1136                nanos = nanos * 10 + u32::from(bytes[cursor] - b'0');
1137            }
1138            cursor += 1;
1139        }
1140        let kept = (cursor - fraction_start).min(9);
1141        if kept == 0 {
1142            return None;
1143        }
1144        nanos *= 10_u32.pow(u32::try_from(9 - kept).ok()?);
1145    }
1146    let datetime = date.and_hms_nano_opt(hour, minute, second, nanos)?;
1147    let offset_seconds = match bytes.get(cursor..) {
1148        Some([]) | Some([b'Z']) | Some([b'z']) => 0,
1149        Some([sign @ (b'+' | b'-'), hour_1, hour_2]) => {
1150            signed_offset(*sign, [*hour_1, *hour_2], *b"00")?
1151        }
1152        Some([sign @ (b'+' | b'-'), hour_1, hour_2, minute_1, minute_2]) => {
1153            signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?
1154        }
1155        Some(
1156            [
1157                sign @ (b'+' | b'-'),
1158                hour_1,
1159                hour_2,
1160                b':',
1161                minute_1,
1162                minute_2,
1163            ],
1164        ) => signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?,
1165        _ => return None,
1166    };
1167    FixedOffset::east_opt(offset_seconds)?
1168        .from_local_datetime(&datetime)
1169        .single()
1170        .map(|timestamp| timestamp.timestamp_millis())
1171}
1172
1173fn signed_offset(sign: u8, hours: [u8; 2], minutes: [u8; 2]) -> Option<i32> {
1174    let pair = |digits: [u8; 2]| {
1175        digits
1176            .iter()
1177            .all(u8::is_ascii_digit)
1178            .then_some(i32::from(digits[0] - b'0') * 10 + i32::from(digits[1] - b'0'))
1179    };
1180    let hours = pair(hours)?;
1181    let minutes = pair(minutes)?;
1182    if hours > 23 || minutes > 59 {
1183        return None;
1184    }
1185    let seconds = hours * 3600 + minutes * 60;
1186    Some(if sign == b'-' { -seconds } else { seconds })
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192    use futures_util::StreamExt;
1193    use teaql_core::{DeleteCommand, Record, RecoverCommand};
1194    use teaql_macros::TeaqlEntity;
1195    use teaql_runtime::InMemoryMetadataStore;
1196
1197    #[test]
1198    fn ensure_schema_registers_soundex_idempotently() {
1199        let executor =
1200            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1201        executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1202        executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1203        let connection = executor.connection();
1204        let guard = connection.lock().unwrap();
1205        let encoded: String = guard
1206            .query_row("SELECT soundex('Robert')", [], |row| row.get(0))
1207            .unwrap();
1208        let matches: i64 = guard
1209            .query_row("SELECT soundex('Robert') = soundex('Rupert')", [], |row| {
1210                row.get(0)
1211            })
1212            .unwrap();
1213        let empty: String = guard
1214            .query_row("SELECT soundex(NULL)", [], |row| row.get(0))
1215            .unwrap();
1216        assert_eq!(encoded, "R163");
1217        assert_eq!(matches, 1);
1218        assert_eq!(empty, "?000");
1219    }
1220
1221    #[test]
1222    fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
1223        let connection = Connection::open_in_memory().unwrap();
1224        connection
1225            .execute_batch(
1226                "CREATE TABLE stream_fixture(id INTEGER);\
1227                 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
1228            )
1229            .unwrap();
1230        let executor = SqliteMutationExecutor::from_connection(connection);
1231        let query = CompiledQuery {
1232            sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
1233            params: vec![],
1234            comment: None,
1235        };
1236        let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
1237        let sizes = futures_executor::block_on(async {
1238            let mut result = Vec::new();
1239            while let Some(chunk) = stream.next().await {
1240                result.push(chunk.unwrap().rows.len());
1241            }
1242            result
1243        });
1244        assert_eq!(sizes, vec![2, 2, 1]);
1245
1246        let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
1247        assert_eq!(
1248            futures_executor::block_on(early.next())
1249                .unwrap()
1250                .unwrap()
1251                .rows
1252                .len(),
1253            2
1254        );
1255        drop(early);
1256        let count: i64 = executor
1257            .connection()
1258            .lock()
1259            .unwrap()
1260            .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
1261            .unwrap();
1262        assert_eq!(count, 5);
1263    }
1264
1265    #[test]
1266    fn decimal_bind_is_numeric_and_comparable() {
1267        let value =
1268            bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
1269        assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
1270        let connection = Connection::open_in_memory().unwrap();
1271        let matches: i64 = connection
1272            .query_row(
1273                "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
1274                [value],
1275                |row| row.get(0),
1276            )
1277            .unwrap();
1278        assert_eq!(matches, 1);
1279    }
1280
1281    #[test]
1282    fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
1283        let connection = Connection::open_in_memory().unwrap();
1284        connection
1285            .execute_batch(
1286                "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
1287            )
1288            .unwrap();
1289        let query = CompiledQuery {
1290            sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
1291            params: vec![
1292                Value::I64(1),
1293                Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
1294                Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
1295            ],
1296            comment: None,
1297        };
1298        let values = bind_values(&query.params).unwrap();
1299        connection
1300            .execute(&query.sql, rusqlite::params_from_iter(values))
1301            .unwrap();
1302        connection
1303            .execute(
1304                &query
1305                    .debug_sql(teaql_sql::DatabaseKind::Sqlite)
1306                    .replace("VALUES (1,", "VALUES (2,"),
1307                [],
1308            )
1309            .unwrap();
1310
1311        let equal_count: i64 = connection.query_row(
1312            "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",
1313            [], |row| row.get(0),
1314        ).unwrap();
1315        let storage_type: String = connection
1316            .query_row(
1317                "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
1318                [],
1319                |row| row.get(0),
1320            )
1321            .unwrap();
1322        assert_eq!(equal_count, 1);
1323        assert_eq!(storage_type, "integer");
1324    }
1325
1326    fn entity() -> EntityDescriptor {
1327        EntityDescriptor::new("Order")
1328            .table_name("orders")
1329            .property(
1330                PropertyDescriptor::new("id", DataType::U64)
1331                    .column_name("id")
1332                    .id()
1333                    .not_null(),
1334            )
1335            .property(
1336                PropertyDescriptor::new("version", DataType::I64)
1337                    .column_name("version")
1338                    .version()
1339                    .not_null(),
1340            )
1341            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1342    }
1343
1344    fn order_line_entity() -> EntityDescriptor {
1345        EntityDescriptor::new("OrderLine")
1346            .table_name("order_line")
1347            .property(
1348                PropertyDescriptor::new("id", DataType::U64)
1349                    .column_name("id")
1350                    .id()
1351                    .not_null(),
1352            )
1353            .property(
1354                PropertyDescriptor::new("order_id", DataType::U64)
1355                    .column_name("order_id")
1356                    .not_null(),
1357            )
1358            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1359    }
1360
1361    fn complete_query_record_entity() -> EntityDescriptor {
1362        EntityDescriptor::new("QueryRecord")
1363            .table_name("query_record_scalar")
1364            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1365            .property(PropertyDescriptor::new("required_text", DataType::Text))
1366            .property(PropertyDescriptor::new("optional_text", DataType::Text))
1367            .property(PropertyDescriptor::new("required_integer", DataType::I64))
1368            .property(PropertyDescriptor::new("optional_long", DataType::I64))
1369            .property(PropertyDescriptor::new(
1370                "required_decimal",
1371                DataType::Decimal,
1372            ))
1373            .property(PropertyDescriptor::new("required_float", DataType::F64))
1374            .property(PropertyDescriptor::new("required_double", DataType::F64))
1375            .property(PropertyDescriptor::new("required_date", DataType::Date))
1376            .property(PropertyDescriptor::new("required_time", DataType::I64))
1377            .property(PropertyDescriptor::new(
1378                "required_timestamp",
1379                DataType::Timestamp,
1380            ))
1381            .property(PropertyDescriptor::new("active", DataType::Bool))
1382            .property(PropertyDescriptor::new("reviewed", DataType::Bool))
1383            .property(
1384                PropertyDescriptor::new("version", DataType::I64)
1385                    .version()
1386                    .not_null(),
1387            )
1388    }
1389
1390    #[test]
1391    fn complete_scalar_fixture_including_nullable_boolean_executes_on_sqlite() {
1392        let executor = SqliteMutationExecutor::from_connection(
1393            Connection::open_in_memory().expect("open SQLite fixture"),
1394        );
1395        executor
1396            .connection()
1397            .lock()
1398            .expect("lock SQLite fixture")
1399            .execute_batch("CREATE TABLE query_record_scalar (\
1400                    id INTEGER PRIMARY KEY, required_text TEXT, optional_text TEXT,\
1401                    required_integer INTEGER, optional_long INTEGER, required_decimal NUMERIC,\
1402                    required_float REAL, required_double REAL, required_date DATE,\
1403                    required_time INTEGER, required_timestamp TIMESTAMP,\
1404                    active BOOLEAN, reviewed BOOLEAN, version INTEGER);\
1405                    INSERT INTO query_record_scalar VALUES \
1406                    (1,'Alpha','optional',42,42000000000,42.125,42.5,42.75,'2026-08-29',34200000,1777632600000,1,0,1),\
1407                    (2,'Beta',NULL,7,NULL,7.500,7.5,7.75,'2026-08-30',36000000,1777720400000,0,NULL,1),\
1408                    (3,'Gamma','tail',99,99000000000,99.875,99.5,99.75,'2026-08-31',37800000,1777808200000,1,1,1)")
1409            .expect("seed complete scalar fixture");
1410        let entity = complete_query_record_entity();
1411        let ids = |expr: Expr| {
1412            let query = SelectQuery::new("QueryRecord")
1413                .project("id")
1414                .filter(expr)
1415                .order_asc("id");
1416            executor
1417                .fetch_all_compact(&SqliteDialect.compile_select(&entity, &query).unwrap())
1418                .expect("execute scalar predicate")
1419                .into_iter()
1420                .map(|row| row.get("id").cloned().expect("projected id"))
1421                .collect::<Vec<_>>()
1422        };
1423        assert_eq!(ids(Expr::eq("required_text", "Alpha")), vec![Value::I64(1)]);
1424        assert_eq!(
1425            ids(Expr::ne("required_text", "Alpha")),
1426            vec![Value::I64(2), Value::I64(3)]
1427        );
1428        assert_eq!(
1429            ids(Expr::in_list(
1430                "required_text",
1431                [Value::from("Alpha"), Value::from("Gamma")]
1432            )),
1433            vec![Value::I64(1), Value::I64(3)]
1434        );
1435        assert_eq!(
1436            ids(Expr::contain("required_text", "et")),
1437            vec![Value::I64(2)]
1438        );
1439        assert_eq!(
1440            ids(Expr::between("required_integer", 40_i64, 100_i64)),
1441            vec![Value::I64(1), Value::I64(3)]
1442        );
1443        assert_eq!(
1444            ids(Expr::gt("required_decimal", Decimal::from(50))),
1445            vec![Value::I64(3)]
1446        );
1447        assert_eq!(
1448            ids(Expr::lte("required_float", 7.5_f64)),
1449            vec![Value::I64(2)]
1450        );
1451        assert_eq!(
1452            ids(Expr::gte("required_double", 99.75_f64)),
1453            vec![Value::I64(3)]
1454        );
1455        assert_eq!(
1456            ids(Expr::between(
1457                "required_date",
1458                NaiveDate::from_ymd_opt(2026, 8, 30).unwrap(),
1459                NaiveDate::from_ymd_opt(2026, 8, 31).unwrap(),
1460            )),
1461            vec![Value::I64(2), Value::I64(3)]
1462        );
1463        assert_eq!(
1464            ids(Expr::gt("required_time", 36_000_000_i64)),
1465            vec![Value::I64(3)]
1466        );
1467        assert_eq!(
1468            ids(Expr::lt(
1469                "required_timestamp",
1470                teaql_core::time::Timestamp(1_777_750_000_000)
1471            )),
1472            vec![Value::I64(1), Value::I64(2)]
1473        );
1474        assert_eq!(ids(Expr::is_null("optional_text")), vec![Value::I64(2)]);
1475        assert_eq!(
1476            ids(Expr::is_not_null("optional_long")),
1477            vec![Value::I64(1), Value::I64(3)]
1478        );
1479        assert_eq!(ids(Expr::eq("active", false)), vec![Value::I64(2)]);
1480        assert_eq!(ids(Expr::eq("reviewed", true)), vec![Value::I64(3)]);
1481        assert_eq!(ids(Expr::eq("reviewed", false)), vec![Value::I64(1)]);
1482        assert_eq!(ids(Expr::is_null("reviewed")), vec![Value::I64(2)]);
1483    }
1484
1485    #[test]
1486    fn relation_subqueries_execute_positive_and_negative_predicates_on_sqlite() {
1487        let executor = SqliteMutationExecutor::from_connection(
1488            Connection::open_in_memory().expect("open SQLite fixture"),
1489        );
1490        executor
1491            .connection()
1492            .lock()
1493            .expect("lock SQLite fixture")
1494            .execute_batch(
1495                "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT);\
1496                 CREATE TABLE order_line (id INTEGER PRIMARY KEY, order_id INTEGER, name TEXT);\
1497                 INSERT INTO orders VALUES (1, 1, 'first'), (2, 1, 'second'), (3, 1, 'third');\
1498                 INSERT INTO order_line VALUES\
1499                    (10, 1, 'priority'), (11, 1, 'ordinary'), (12, 2, 'ordinary'),\
1500                    (13, NULL, 'orphan');",
1501            )
1502            .expect("seed relation fixture");
1503
1504        let matching_lines = SelectQuery::new("OrderLine").filter(Expr::eq("name", "priority"));
1505        let positive = SelectQuery::new("Order")
1506            .project("id")
1507            .filter(Expr::in_subquery(
1508                "id",
1509                order_line_entity(),
1510                matching_lines.clone(),
1511                "order_id",
1512            ))
1513            .order_asc("id");
1514        let negative = SelectQuery::new("Order")
1515            .project("id")
1516            .filter(Expr::not_in_subquery(
1517                "id",
1518                order_line_entity(),
1519                matching_lines,
1520                "order_id",
1521            ))
1522            .order_asc("id");
1523
1524        let ids = |rows: Vec<CompactRow>| {
1525            rows.into_iter()
1526                .map(|row| row.get("id").cloned().expect("projected id"))
1527                .collect::<Vec<_>>()
1528        };
1529        let order_ids = |query: SelectQuery| {
1530            ids(executor
1531                .fetch_all_compact(&SqliteDialect.compile_select(&entity(), &query).unwrap())
1532                .expect("execute order relation predicate"))
1533        };
1534        let line_ids = |query: SelectQuery| {
1535            ids(executor
1536                .fetch_all_compact(
1537                    &SqliteDialect
1538                        .compile_select(&order_line_entity(), &query)
1539                        .unwrap(),
1540                )
1541                .expect("execute line relation predicate"))
1542        };
1543
1544        // Reverse relation: typed child matching and its negative form.
1545        assert_eq!(order_ids(positive), vec![Value::I64(1)]);
1546        assert_eq!(order_ids(negative), vec![Value::I64(2), Value::I64(3)]);
1547
1548        // Forward relation identity state keeps NULL distinct from a known FK.
1549        assert_eq!(
1550            line_ids(
1551                SelectQuery::new("OrderLine")
1552                    .project("id")
1553                    .filter(Expr::is_not_null("order_id"))
1554                    .order_asc("id")
1555            ),
1556            vec![Value::I64(10), Value::I64(11), Value::I64(12)]
1557        );
1558        assert_eq!(
1559            line_ids(
1560                SelectQuery::new("OrderLine")
1561                    .project("id")
1562                    .filter(Expr::is_null("order_id"))
1563                    .order_asc("id")
1564            ),
1565            vec![Value::I64(13)]
1566        );
1567
1568        // Forward nested matching. SQL NOT IN deliberately excludes the NULL
1569        // foreign key; callers use IsUnknown when they want orphan rows.
1570        let first_order = SelectQuery::new("Order").filter(Expr::eq("name", "first"));
1571        assert_eq!(
1572            line_ids(
1573                SelectQuery::new("OrderLine")
1574                    .project("id")
1575                    .filter(Expr::in_subquery(
1576                        "order_id",
1577                        entity(),
1578                        first_order.clone(),
1579                        "id",
1580                    ))
1581                    .order_asc("id")
1582            ),
1583            vec![Value::I64(10), Value::I64(11)]
1584        );
1585        assert_eq!(
1586            line_ids(
1587                SelectQuery::new("OrderLine")
1588                    .project("id")
1589                    .filter(Expr::not_in_subquery(
1590                        "order_id",
1591                        entity(),
1592                        first_order,
1593                        "id",
1594                    ))
1595                    .order_asc("id")
1596            ),
1597            vec![Value::I64(12)]
1598        );
1599
1600        // Reverse existence/non-existence without an additional child filter.
1601        let all_lines = SelectQuery::new("OrderLine");
1602        assert_eq!(
1603            order_ids(
1604                SelectQuery::new("Order")
1605                    .project("id")
1606                    .filter(Expr::in_subquery(
1607                        "id",
1608                        order_line_entity(),
1609                        all_lines.clone(),
1610                        "order_id",
1611                    ))
1612                    .order_asc("id")
1613            ),
1614            vec![Value::I64(1), Value::I64(2)]
1615        );
1616        assert_eq!(
1617            order_ids(
1618                SelectQuery::new("Order")
1619                    .project("id")
1620                    .filter(Expr::not_in_subquery(
1621                        "id",
1622                        order_line_entity(),
1623                        all_lines,
1624                        "order_id",
1625                    ))
1626                    .order_asc("id")
1627            ),
1628            vec![Value::I64(3)]
1629        );
1630    }
1631
1632    #[allow(dead_code)]
1633    #[derive(Debug, PartialEq, TeaqlEntity)]
1634    #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1635    struct FeatureFlagRow {
1636        #[teaql(id)]
1637        id: u64,
1638        #[teaql(version)]
1639        version: i64,
1640        enabled: bool,
1641        optional_enabled: Option<bool>,
1642    }
1643
1644    fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1645        Record::from([
1646            ("id".to_owned(), Value::U64(1)),
1647            ("version".to_owned(), Value::I64(1)),
1648            ("enabled".to_owned(), enabled),
1649            ("optional_enabled".to_owned(), optional_enabled),
1650        ])
1651    }
1652
1653    #[test]
1654    fn sqlite_dialect_compiles_mutations_and_schema() {
1655        assert!(SqliteDialect.prefers_small_parent_relation_probes());
1656        let insert = SqliteDialect
1657            .compile_insert(
1658                &entity(),
1659                &InsertCommand::new("Order")
1660                    .value("id", 1_u64)
1661                    .value("name", "A"),
1662            )
1663            .unwrap();
1664        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1665
1666        let update = SqliteDialect
1667            .compile_update(
1668                &entity(),
1669                &UpdateCommand::new("Order", 1_u64)
1670                    .expected_version(3)
1671                    .value("name", "B"),
1672            )
1673            .unwrap();
1674        assert_eq!(
1675            update.sql,
1676            "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1677        );
1678
1679        let delete = SqliteDialect
1680            .compile_delete(
1681                &entity(),
1682                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1683            )
1684            .unwrap();
1685        let recover = SqliteDialect
1686            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1687            .unwrap();
1688        assert_eq!(
1689            delete.sql,
1690            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1691        );
1692        assert_eq!(
1693            recover.sql,
1694            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1695        );
1696
1697        let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1698        assert_eq!(
1699            create,
1700            "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1701        );
1702    }
1703
1704    #[test]
1705    fn column_layout_cache_uses_parameterized_sql_not_comments() {
1706        let connection = Connection::open_in_memory().unwrap();
1707        connection
1708            .execute("CREATE TABLE sample (id INTEGER, enabled BOOLEAN)", [])
1709            .unwrap();
1710        connection
1711            .execute("INSERT INTO sample (id, enabled) VALUES (1, 1)", [])
1712            .unwrap();
1713        let executor = SqliteMutationExecutor::from_connection(connection);
1714        let mut first = CompiledQuery {
1715            sql: "SELECT id, enabled FROM sample WHERE id = ?".to_owned(),
1716            params: vec![Value::I64(1)],
1717            comment: Some("first purpose".to_owned()),
1718        };
1719        let rows = executor.fetch_all_compact(&first).unwrap();
1720        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(true)));
1721
1722        first.comment = Some("different purpose".to_owned());
1723        executor.fetch_all_compact(&first).unwrap();
1724
1725        assert_eq!(executor.column_layout_cache.lock().unwrap().len(), 1);
1726    }
1727
1728    #[test]
1729    fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1730        let executor =
1731            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1732        let entity = entity();
1733        let mut context = UserContext::new()
1734            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1735
1736        context.use_sqlite_provider(executor.clone());
1737        ensure_sqlite_schema_for(&context).unwrap();
1738
1739        let insert = SqliteDialect
1740            .compile_insert(
1741                &entity,
1742                &InsertCommand::new("Order")
1743                    .value("id", 1_u64)
1744                    .value("version", 1_i64)
1745                    .value("name", "draft"),
1746            )
1747            .unwrap();
1748        assert_eq!(executor.execute(&insert).unwrap(), 1);
1749
1750        let select = SqliteDialect
1751            .compile_select(
1752                &entity,
1753                &SelectQuery::new("Order")
1754                    .filter(Expr::eq("id", 1_u64))
1755                    .order_asc("id"),
1756            )
1757            .unwrap();
1758        let rows = executor.fetch_all_compact(&select).unwrap();
1759        assert_eq!(rows.len(), 1);
1760        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1761        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1762        assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
1763    }
1764
1765    #[test]
1766    fn repeated_schema_ensure_does_not_overwrite_existing_initial_graph() {
1767        let executor =
1768            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1769        let entity = entity();
1770        let mut context = UserContext::new()
1771            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1772        context.set_root_graphs(vec![
1773            GraphNode::new("Order")
1774                .value("id", 1_u64)
1775                .value("version", 1_i64)
1776                .value("name", "module seed"),
1777        ]);
1778        context.use_sqlite_provider(executor.clone());
1779
1780        ensure_sqlite_schema_for(&context).unwrap();
1781        let customize = SqliteDialect
1782            .compile_update(
1783                &entity,
1784                &UpdateCommand::new("Order", 1_u64).value("name", "application value"),
1785            )
1786            .unwrap();
1787        assert_eq!(executor.execute(&customize).unwrap(), 1);
1788
1789        ensure_sqlite_schema_for(&context).unwrap();
1790
1791        let select = SqliteDialect
1792            .compile_select(
1793                &entity,
1794                &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1795            )
1796            .unwrap();
1797        let rows = executor.fetch_all_compact(&select).unwrap();
1798        assert_eq!(rows.len(), 1);
1799        assert_eq!(
1800            rows[0].get("name"),
1801            Some(&Value::Text("application value".to_owned()))
1802        );
1803    }
1804
1805    #[test]
1806    fn repeated_schema_ensure_reconciles_changed_constant_graph() {
1807        let executor =
1808            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1809        let entity = entity();
1810        let mut context = UserContext::new()
1811            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1812        context.set_initial_graphs(vec![
1813            GraphNode::new("Order")
1814                .value("id", 1001_u64)
1815                .value("version", 1_i64)
1816                .value("name", "red"),
1817        ]);
1818        context.use_sqlite_provider(executor.clone());
1819        ensure_sqlite_schema_for(&context).unwrap();
1820        ensure_sqlite_schema_for(&context).unwrap();
1821
1822        let unchanged = SqliteDialect
1823            .compile_select(
1824                &entity,
1825                &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1826            )
1827            .unwrap();
1828        let rows = executor.fetch_all_compact(&unchanged).unwrap();
1829        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1830
1831        context.set_initial_graphs(vec![
1832            GraphNode::new("Order")
1833                .value("id", 1001_u64)
1834                .value("version", 1_i64)
1835                .value("name", "crimson"),
1836        ]);
1837        ensure_sqlite_schema_for(&context).unwrap();
1838
1839        let select = SqliteDialect
1840            .compile_select(
1841                &entity,
1842                &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1843            )
1844            .unwrap();
1845        let rows = executor.fetch_all_compact(&select).unwrap();
1846        assert_eq!(
1847            rows[0].get("name"),
1848            Some(&Value::Text("crimson".to_owned()))
1849        );
1850        assert_eq!(rows[0].get("version"), Some(&Value::I64(2)));
1851        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1852        assert_eq!(generator.next_id("Order").unwrap(), 1002);
1853    }
1854
1855    #[test]
1856    fn sqlite_executes_partitioned_relation_limit_per_parent() {
1857        let executor =
1858            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1859        let entity = order_line_entity();
1860        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1861
1862        for order_id in [11_u64, 12_u64] {
1863            for index in 1_u64..=5 {
1864                let id = order_id * 100 + index;
1865                let insert = SqliteDialect
1866                    .compile_insert(
1867                        &entity,
1868                        &InsertCommand::new("OrderLine")
1869                            .value("id", id)
1870                            .value("order_id", order_id)
1871                            .value("name", format!("line-{id}")),
1872                    )
1873                    .unwrap();
1874                executor.execute(&insert).unwrap();
1875            }
1876        }
1877
1878        let query = SelectQuery::new("OrderLine")
1879            .project("id")
1880            .project("order_id")
1881            .order_desc("id")
1882            .limit(3)
1883            .partition_by("order_id");
1884        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1885        let rows = executor.fetch_all_compact(&compiled).unwrap();
1886
1887        assert_eq!(rows.len(), 6);
1888        for order_id in [11_i64, 12_i64] {
1889            let ids = rows
1890                .iter()
1891                .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
1892                .filter_map(|row| row.get("id").cloned())
1893                .collect::<Vec<_>>();
1894            assert_eq!(
1895                ids,
1896                vec![
1897                    Value::I64(order_id * 100 + 5),
1898                    Value::I64(order_id * 100 + 4),
1899                    Value::I64(order_id * 100 + 3),
1900                ]
1901            );
1902        }
1903    }
1904
1905    #[test]
1906    fn topn_005_007_window_and_probes_preserve_results_and_predicates() {
1907        futures_executor::block_on(async {
1908            #[derive(Clone)]
1909            struct FixedSchema(Arc<EntityDescriptor>);
1910
1911            impl teaql_data_service::SchemaProvider for FixedSchema {
1912                fn get_entity(&self, name: &str) -> Option<Arc<EntityDescriptor>> {
1913                    (name == self.0.name).then(|| self.0.clone())
1914                }
1915            }
1916
1917            let transport =
1918                SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1919            let entity = Arc::new(order_line_entity());
1920            transport
1921                .ensure_schema(&SqliteDialect, &[entity.as_ref()])
1922                .unwrap();
1923
1924            for order_id in [11_u64, 12_u64, 13_u64] {
1925                for index in 1_u64..=5 {
1926                    let id = order_id * 100 + index;
1927                    let name = if index == 4 { "excluded" } else { "visible" };
1928                    let insert = SqliteDialect
1929                        .compile_insert(
1930                            &entity,
1931                            &InsertCommand::new("OrderLine")
1932                                .value("id", id)
1933                                .value("order_id", order_id)
1934                                .value("name", name),
1935                        )
1936                        .unwrap();
1937                    transport.execute(&insert).unwrap();
1938                }
1939            }
1940
1941            let executor = teaql_sql::SqlDataServiceExecutor::new(
1942                SqliteDialect,
1943                transport,
1944                FixedSchema(entity),
1945            );
1946            let base = SelectQuery::new("OrderLine")
1947                .project("id")
1948                .project("order_id")
1949                .project("name")
1950                .filter(Expr::in_list("order_id", [Value::U64(11), Value::U64(12)]))
1951                .and_filter(Expr::eq("name", "visible"))
1952                .order_desc("id")
1953                .limit(3)
1954                .partition_by("order_id");
1955            let execute = |query| {
1956                teaql_data_service::QueryExecutor::query(
1957                    &executor,
1958                    teaql_data_service::QueryRequest {
1959                        query,
1960                        trace_chain: Vec::new(),
1961                        comment: Some("TOPN plan equivalence".to_owned()),
1962                        capture_debug_query: false,
1963                        capture_execution_metadata: false,
1964                    },
1965                )
1966            };
1967
1968            let probes = execute(base.clone()).await.unwrap().rows;
1969            let window = execute(base.top_n_probe_parent_threshold(0))
1970                .await
1971                .unwrap()
1972                .rows;
1973            let children_of = |rows: &[CompactRow], parent: i64| {
1974                rows.iter()
1975                    .filter(|row| row.get("order_id") == Some(&Value::I64(parent)))
1976                    .map(|row| (row.get("id").cloned(), row.get("name").cloned()))
1977                    .collect::<Vec<_>>()
1978            };
1979
1980            for parent in [11_i64, 12_i64] {
1981                assert_eq!(children_of(&probes, parent), children_of(&window, parent));
1982            }
1983            assert_eq!(
1984                children_of(&window, 11),
1985                vec![
1986                    (Some(Value::I64(1105)), Some(Value::Text("visible".into()))),
1987                    (Some(Value::I64(1103)), Some(Value::Text("visible".into()))),
1988                    (Some(Value::I64(1102)), Some(Value::Text("visible".into()))),
1989                ]
1990            );
1991            assert_eq!(
1992                children_of(&window, 12),
1993                vec![
1994                    (Some(Value::I64(1205)), Some(Value::Text("visible".into()))),
1995                    (Some(Value::I64(1203)), Some(Value::Text("visible".into()))),
1996                    (Some(Value::I64(1202)), Some(Value::Text("visible".into()))),
1997                ]
1998            );
1999            assert!(children_of(&probes, 13).is_empty());
2000            assert!(children_of(&window, 13).is_empty());
2001        });
2002    }
2003
2004    #[test]
2005    fn sqlite_boolean_new_schema_roundtrips_as_bool() {
2006        let executor =
2007            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2008        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2009        let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
2010        assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
2011        assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
2012        assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
2013
2014        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
2015        for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
2016            let insert = SqliteDialect
2017                .compile_insert(
2018                    &entity,
2019                    &InsertCommand::new("FeatureFlag")
2020                        .value("id", id)
2021                        .value("version", 1_i64)
2022                        .value("enabled", enabled)
2023                        .value("optional_enabled", optional_enabled),
2024                )
2025                .unwrap();
2026            assert_eq!(executor.execute(&insert).unwrap(), 1);
2027        }
2028
2029        let select = SqliteDialect
2030            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2031            .unwrap();
2032        let rows = executor.fetch_all_compact(&select).unwrap();
2033        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
2034        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
2035        assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
2036        assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
2037
2038        let first =
2039            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2040        let second =
2041            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap();
2042        assert!(!first.enabled);
2043        assert_eq!(first.optional_enabled, Some(true));
2044        assert!(second.enabled);
2045        assert_eq!(second.optional_enabled, Some(false));
2046    }
2047
2048    #[test]
2049    fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
2050        let executor =
2051            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2052        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2053        executor
2054            .execute(&CompiledQuery {
2055                sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
2056                    .to_owned(),
2057                params: Vec::new(),
2058                comment: None,
2059            })
2060            .unwrap();
2061
2062        let insert = SqliteDialect
2063            .compile_insert(
2064                &entity,
2065                &InsertCommand::new("FeatureFlag")
2066                    .value("id", 1_u64)
2067                    .value("version", 1_i64)
2068                    .value("enabled", true)
2069                    .value("optional_enabled", false),
2070            )
2071            .unwrap();
2072        executor.execute(&insert).unwrap();
2073        executor
2074            .execute(&CompiledQuery {
2075                sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
2076                    .to_owned(),
2077                params: vec![
2078                    Value::U64(2),
2079                    Value::I64(1),
2080                    Value::I64(2),
2081                    Value::Null,
2082                ],
2083                comment: None,
2084            })
2085            .unwrap();
2086        let select = SqliteDialect
2087            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2088            .unwrap();
2089        let rows = executor.fetch_all_compact(&select).unwrap();
2090        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
2091        assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
2092        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
2093
2094        let decoded =
2095            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2096        assert!(decoded.enabled);
2097        assert_eq!(decoded.optional_enabled, Some(false));
2098        assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
2099        let error =
2100            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap_err();
2101        assert!(error.message.contains("invalid field enabled"));
2102
2103        for (value, expected) in [
2104            (Value::I64(0), false),
2105            (Value::I64(1), true),
2106            (Value::U64(0), false),
2107            (Value::U64(1), true),
2108        ] {
2109            let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2110                teaql_core::CompactRow::from_map(feature_flag_record(value, Value::Null)),
2111            )
2112            .unwrap();
2113            assert_eq!(decoded.enabled, expected);
2114            assert_eq!(decoded.optional_enabled, None);
2115        }
2116
2117        for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
2118            let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2119                teaql_core::CompactRow::from_map(feature_flag_record(invalid, Value::Null)),
2120            )
2121            .unwrap_err();
2122            assert!(error.message.contains("invalid field enabled"));
2123        }
2124        let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2125            teaql_core::CompactRow::from_map(feature_flag_record(Value::Bool(true), Value::U64(2))),
2126        )
2127        .unwrap_err();
2128        assert!(error.message.contains("invalid field optional_enabled"));
2129    }
2130
2131    #[test]
2132    fn sqlite_executor_parses_json_only_for_json_columns() {
2133        let executor =
2134            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2135
2136        executor
2137            .execute(&CompiledQuery {
2138                sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
2139                params: Vec::new(),
2140                comment: None,
2141            })
2142            .unwrap();
2143        executor
2144            .execute(&CompiledQuery {
2145                sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
2146                params: vec![
2147                    Value::Text("{\"active\":true}".to_owned()),
2148                    Value::Json(serde_json::json!({"active": true})),
2149                ],
2150                comment: None,
2151            })
2152            .unwrap();
2153
2154        let rows = executor
2155            .fetch_all_compact(&CompiledQuery {
2156                sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
2157                params: Vec::new(),
2158                comment: None,
2159            })
2160            .unwrap();
2161
2162        assert_eq!(
2163            rows[0].get("text_payload"),
2164            Some(&Value::Text("{\"active\":true}".to_owned()))
2165        );
2166        assert_eq!(
2167            rows[0].get("json_payload"),
2168            Some(&Value::Json(serde_json::json!({"active": true})))
2169        );
2170    }
2171
2172    #[test]
2173    fn sqlite_id_space_generator_increments_ids() {
2174        let executor =
2175            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2176        let generator = SqliteIdSpaceGenerator::from_executor(executor);
2177        assert_eq!(generator.next_id("Order").unwrap(), 1);
2178        assert_eq!(generator.next_id("Order").unwrap(), 2);
2179    }
2180
2181    #[test]
2182    fn sqlite_id_space_generator_is_safe_across_connections() {
2183        let path = std::env::temp_dir().join(format!(
2184            "teaql-id-space-{}-{}.db",
2185            std::process::id(),
2186            std::time::SystemTime::now()
2187                .duration_since(std::time::UNIX_EPOCH)
2188                .unwrap()
2189                .as_nanos()
2190        ));
2191        let mut workers = Vec::new();
2192        for _ in 0..4 {
2193            let path = path.clone();
2194            workers.push(std::thread::spawn(move || {
2195                let connection = Connection::open(path).unwrap();
2196                connection
2197                    .busy_timeout(std::time::Duration::from_secs(5))
2198                    .unwrap();
2199                let generator = SqliteIdSpaceGenerator::new(connection);
2200                (0..25)
2201                    .map(|_| generator.next_id("Order").unwrap())
2202                    .collect::<Vec<_>>()
2203            }));
2204        }
2205        let mut ids = workers
2206            .into_iter()
2207            .flat_map(|worker| worker.join().unwrap())
2208            .collect::<Vec<_>>();
2209        ids.sort_unstable();
2210        assert_eq!(ids, (1..=100).collect::<Vec<_>>());
2211        let _ = std::fs::remove_file(path);
2212    }
2213
2214    #[test]
2215    fn sqlite_fetch_stream_returns_chunked_rows() {
2216        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2217            Connection::open_in_memory().unwrap(),
2218        )));
2219        let entity = entity();
2220
2221        // Create table and insert 25 rows
2222        executor
2223            .execute(&CompiledQuery {
2224                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2225                    .to_owned(),
2226                params: Vec::new(),
2227                comment: None,
2228            })
2229            .unwrap();
2230
2231        for i in 1..=25 {
2232            let insert = SqliteDialect
2233                .compile_insert(
2234                    &entity,
2235                    &InsertCommand::new("Order")
2236                        .value("id", i as u64)
2237                        .value("version", 1_i64)
2238                        .value("name", format!("order-{i}")),
2239                )
2240                .unwrap();
2241            executor.execute(&insert).unwrap();
2242        }
2243
2244        // Stream with chunk_size = 10
2245        let query = SelectQuery::new("Order")
2246            .filter(Expr::gt("version", 0_i64))
2247            .order_asc("id")
2248            .stream(10);
2249
2250        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2251
2252        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2253
2254        // 25 rows / 10 per chunk = 3 chunks
2255        assert_eq!(chunks.len(), 3);
2256        assert_eq!(chunks[0].rows.len(), 10);
2257        assert_eq!(chunks[0].chunk_index, 0);
2258        assert!(!chunks[0].is_last);
2259
2260        assert_eq!(chunks[1].rows.len(), 10);
2261        assert_eq!(chunks[1].chunk_index, 1);
2262        assert!(!chunks[1].is_last);
2263
2264        assert_eq!(chunks[2].rows.len(), 5);
2265        assert_eq!(chunks[2].chunk_index, 2);
2266        assert!(chunks[2].is_last);
2267
2268        // Verify first and last row
2269        assert_eq!(
2270            chunks[0].rows[0].get("name"),
2271            Some(&Value::Text("order-1".to_owned()))
2272        );
2273        assert_eq!(
2274            chunks[2].rows[4].get("name"),
2275            Some(&Value::Text("order-25".to_owned()))
2276        );
2277    }
2278
2279    #[test]
2280    fn sqlite_fetch_stream_handles_empty_result() {
2281        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2282            Connection::open_in_memory().unwrap(),
2283        )));
2284
2285        executor
2286            .execute(&CompiledQuery {
2287                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2288                    .to_owned(),
2289                params: Vec::new(),
2290                comment: None,
2291            })
2292            .unwrap();
2293
2294        let entity = entity();
2295        let query = SelectQuery::new("Order")
2296            .filter(Expr::gt("version", 0_i64))
2297            .stream(10);
2298
2299        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2300
2301        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2302
2303        // Empty result = 1 chunk with 0 rows, marked as last
2304        assert_eq!(chunks.len(), 1);
2305        assert_eq!(chunks[0].rows.len(), 0);
2306        assert!(chunks[0].is_last);
2307    }
2308
2309    #[test]
2310    fn sqlite_fetch_stream_exact_chunk_boundary() {
2311        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2312            Connection::open_in_memory().unwrap(),
2313        )));
2314        let entity = entity();
2315
2316        executor
2317            .execute(&CompiledQuery {
2318                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2319                    .to_owned(),
2320                params: Vec::new(),
2321                comment: None,
2322            })
2323            .unwrap();
2324
2325        // Insert exactly 20 rows
2326        for i in 1..=20 {
2327            let insert = SqliteDialect
2328                .compile_insert(
2329                    &entity,
2330                    &InsertCommand::new("Order")
2331                        .value("id", i as u64)
2332                        .value("version", 1_i64)
2333                        .value("name", format!("order-{i}")),
2334                )
2335                .unwrap();
2336            executor.execute(&insert).unwrap();
2337        }
2338
2339        let query = SelectQuery::new("Order")
2340            .filter(Expr::gt("version", 0_i64))
2341            .order_asc("id")
2342            .stream(10);
2343
2344        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2345
2346        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2347
2348        // 20 rows / 10 per chunk = 2 full chunks + 1 empty final chunk
2349        assert_eq!(chunks.len(), 3);
2350        assert_eq!(chunks[0].rows.len(), 10);
2351        assert!(!chunks[0].is_last);
2352        assert_eq!(chunks[1].rows.len(), 10);
2353        assert!(!chunks[1].is_last);
2354        assert_eq!(chunks[2].rows.len(), 0);
2355        assert!(chunks[2].is_last);
2356    }
2357
2358    #[test]
2359    fn test_parse_sqlite_timestamp() {
2360        let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
2361        assert!(matches!(ts1, Value::Timestamp(_)));
2362
2363        let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
2364        assert!(matches!(ts2, Value::Timestamp(_)));
2365
2366        let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
2367        assert!(matches!(ts3, Value::Timestamp(_)));
2368
2369        let ts4 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546+00").unwrap();
2370        assert!(matches!(ts4, Value::Timestamp(_)));
2371
2372        let ts5 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546").unwrap();
2373        assert!(matches!(ts5, Value::Timestamp(_)));
2374
2375        assert_eq!(
2376            parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+00"),
2377            Some(1_704_067_200_000)
2378        );
2379        assert_eq!(
2380            parse_fixed_sqlite_timestamp("2024-01-01T08:00:00.123+08:00"),
2381            Some(1_704_067_200_123)
2382        );
2383        assert_eq!(
2384            parse_fixed_sqlite_timestamp("2023-12-31 19:00:00-0500"),
2385            Some(1_704_067_200_000)
2386        );
2387        assert_eq!(parse_fixed_sqlite_timestamp("2024-13-01 00:00:00Z"), None);
2388        assert_eq!(parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+24"), None);
2389
2390        assert!(parse_sqlite_timestamp("invalid").is_err());
2391    }
2392
2393    #[test]
2394    fn declared_text_does_not_infer_timestamp_from_content() {
2395        for decl_type in ["TEXT", "VARCHAR(255)", "CHAR(32)", "CLOB"] {
2396            let column = ColumnInfo {
2397                name: "external_timestamp".to_owned(),
2398                decode_kind: sqlite_decode_kind(Some(decl_type)),
2399            };
2400
2401            assert_eq!(
2402                decode_sqlite_text(b"2024-01-01 00:57:55", &column).unwrap(),
2403                Value::Text("2024-01-01 00:57:55".to_owned())
2404            );
2405        }
2406    }
2407
2408    #[test]
2409    fn declared_column_types_compile_to_decode_kinds() {
2410        assert_eq!(sqlite_decode_kind(Some("BOOLEAN")), SqliteDecodeKind::Bool);
2411        assert_eq!(
2412            sqlite_decode_kind(Some("decimal(20, 4)")),
2413            SqliteDecodeKind::Decimal
2414        );
2415        assert_eq!(
2416            sqlite_decode_kind(Some(" VARCHAR(255) ")),
2417            SqliteDecodeKind::Text
2418        );
2419        assert_eq!(
2420            sqlite_decode_kind(Some("datetime")),
2421            SqliteDecodeKind::Timestamp
2422        );
2423        assert_eq!(sqlite_decode_kind(Some("custom")), SqliteDecodeKind::Infer);
2424        assert_eq!(sqlite_decode_kind(None), SqliteDecodeKind::Infer);
2425    }
2426}