Skip to main content

teaql_provider_sqlite/
lib.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::future::Future;
3use std::pin::Pin;
4use std::str::FromStr;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use chrono::{DateTime, NaiveDate, NaiveDateTime};
8use rusqlite::types::{Value as SqliteValue, ValueRef};
9use rusqlite::{Connection, OptionalExtension, Row, params, params_from_iter};
10use rust_decimal::Decimal;
11use teaql_core::{
12    CompactRow, DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, SelectQuery,
13    UpdateCommand, Value,
14};
15use teaql_runtime::{
16    GraphNode, InternalIdGenerator, RawAuditEvent, RuntimeError, SchemaProvider, UserContext,
17    canonical_id_space_entity,
18};
19use teaql_sql::{
20    CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
21    quote_identifier_if_needed,
22};
23
24pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
25
26#[derive(Debug, Default, Clone, Copy)]
27pub struct SqliteDialect;
28
29impl SqlDialect for SqliteDialect {
30    fn kind(&self) -> DatabaseKind {
31        DatabaseKind::Sqlite
32    }
33
34    fn quote_ident(&self, ident: &str) -> String {
35        quote_ident(ident)
36    }
37
38    fn placeholder(&self, _index: usize) -> String {
39        "?".to_owned()
40    }
41
42    fn schema_type_sql(
43        &self,
44        data_type: DataType,
45        property: &PropertyDescriptor,
46    ) -> Result<&'static str, SqlCompileError> {
47        match data_type {
48            DataType::Bool => Ok("BOOLEAN"),
49            DataType::I64 | DataType::U64 if property.is_id => Ok("INTEGER"),
50            DataType::I64 | DataType::U64 => Ok("INTEGER"),
51            DataType::F64 => Ok("REAL"),
52            DataType::Decimal => Ok("NUMERIC"),
53            DataType::Text => Ok("VARCHAR(255)"),
54            DataType::LargeText => Ok("TEXT"),
55            DataType::Json => Ok("JSON"),
56            DataType::Date => Ok("DATE"),
57            DataType::Timestamp => Ok("TIMESTAMP"),
58        }
59    }
60
61    fn compile_add_column(
62        &self,
63        entity: &EntityDescriptor,
64        property: &PropertyDescriptor,
65    ) -> Result<String, SqlCompileError> {
66        // SQLite does not support adding NOT NULL columns without a DEFAULT.
67        // Since TeaQL enforces nullability at the application layer, we can safely
68        // strip the NOT NULL constraint when adding columns to existing tables.
69        let def = self.column_definition_sql(property)?;
70        let def_without_not_null = def.replace(" NOT NULL", "");
71
72        Ok(format!(
73            "ALTER TABLE {} ADD COLUMN {}",
74            self.quote_ident(&entity.table_name),
75            def_without_not_null
76        ))
77    }
78}
79
80#[derive(Debug)]
81pub enum MutationExecutorError {
82    Sqlite(rusqlite::Error),
83    SqlCompile(SqlCompileError),
84    UnsupportedValue(&'static str),
85    UnsupportedColumnType(String),
86    Bind(String),
87    Lock(String),
88}
89
90impl std::fmt::Display for MutationExecutorError {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Self::Sqlite(err) => err.fmt(f),
94            Self::SqlCompile(err) => err.fmt(f),
95            Self::UnsupportedValue(kind) => {
96                write!(
97                    f,
98                    "unsupported rusqlite bind value for mutation executor: {kind}"
99                )
100            }
101            Self::UnsupportedColumnType(kind) => {
102                write!(
103                    f,
104                    "unsupported rusqlite column type for record decoding: {kind}"
105                )
106            }
107            Self::Bind(message) => write!(f, "rusqlite bind error: {message}"),
108            Self::Lock(message) => write!(f, "rusqlite connection lock error: {message}"),
109        }
110    }
111}
112
113impl std::error::Error for MutationExecutorError {}
114
115impl From<rusqlite::Error> for MutationExecutorError {
116    fn from(value: rusqlite::Error) -> Self {
117        Self::Sqlite(value)
118    }
119}
120
121impl From<SqlCompileError> for MutationExecutorError {
122    fn from(value: SqlCompileError) -> Self {
123        Self::SqlCompile(value)
124    }
125}
126
127#[derive(Clone)]
128pub struct SqliteMutationExecutor {
129    connection: Arc<Mutex<Connection>>,
130}
131
132impl SqliteMutationExecutor {
133    pub fn new(connection: Arc<Mutex<Connection>>) -> Self {
134        Self { connection }
135    }
136
137    pub fn from_connection(connection: Connection) -> Self {
138        Self::new(Arc::new(Mutex::new(connection)))
139    }
140
141    pub fn connection(&self) -> Arc<Mutex<Connection>> {
142        Arc::clone(&self.connection)
143    }
144
145    pub fn ensure_schema(
146        &self,
147        dialect: &SqliteDialect,
148        entities: &[&EntityDescriptor],
149    ) -> Result<(), MutationExecutorError> {
150        self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
151
152        for entity in entities {
153            if !self.table_exists(&entity.table_name)? {
154                let sql = dialect.compile_create_table(entity)?;
155                self.lock()?.execute(&sql, [])?;
156                continue;
157            }
158
159            let existing_columns = self.table_columns(&entity.table_name)?;
160            for property in &entity.properties {
161                let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
162                if existing_columns.contains(&bare_column) {
163                    continue;
164                }
165                let sql = dialect.compile_add_column(entity, property)?;
166                self.lock()?.execute(&sql, [])?;
167            }
168
169            for sql in dialect.schema_indexes_sqls(entity)? {
170                self.lock()?.execute(&sql, [])?;
171            }
172        }
173        Ok(())
174    }
175
176    pub fn ensure_id_space_table(&self, table_name: &str) -> Result<(), MutationExecutorError> {
177        let sql = format!(
178            "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
179            quote_ident(table_name)
180        );
181        self.lock()?.execute(&sql, [])?;
182        Ok(())
183    }
184
185    pub fn begin_transaction(&self) -> Result<(), MutationExecutorError> {
186        self.lock()?.execute("BEGIN IMMEDIATE", [])?;
187        Ok(())
188    }
189
190    pub fn commit_transaction(&self) -> Result<(), MutationExecutorError> {
191        self.lock()?.execute("COMMIT", [])?;
192        Ok(())
193    }
194
195    pub fn rollback_transaction(&self) -> Result<(), MutationExecutorError> {
196        self.lock()?.execute("ROLLBACK", [])?;
197        Ok(())
198    }
199
200    pub fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
201        let params = bind_values(&query.params)?;
202        let rows = self
203            .lock()?
204            .execute(&query.sql_with_comment(), params_from_iter(params.iter()))?;
205        Ok(rows as u64)
206    }
207
208    pub fn fetch_all_compact(
209        &self,
210        query: &CompiledQuery,
211    ) -> Result<Vec<CompactRow>, MutationExecutorError> {
212        let params = bind_values(&query.params)?;
213        let connection = self.lock()?;
214        let mut statement = connection.prepare(&query.sql_with_comment())?;
215        let columns = statement_columns(&statement);
216        let column_names: Arc<[String]> = columns
217            .iter()
218            .map(|column| column.name.clone())
219            .collect::<Vec<_>>()
220            .into();
221        let mut rows = statement.query(params_from_iter(params.iter()))?;
222        let mut result = Vec::new();
223        while let Some(row) = rows.next()? {
224            result.push(CompactRow::new(
225                column_names.clone(),
226                decode_sqlite_values(row, &columns)?,
227            ));
228        }
229        Ok(result)
230    }
231
232    /// Fetch rows in streaming mode (chunked).
233    /// Returns a Vec of StreamChunk, each containing up to `chunk_size` rows.
234    pub fn fetch_stream(
235        &self,
236        query: &CompiledQuery,
237        chunk_size: usize,
238    ) -> Result<Vec<teaql_data_service::StreamChunk>, MutationExecutorError> {
239        let params = bind_values(&query.params)?;
240        let connection = self.lock()?;
241        let mut statement = connection.prepare(&query.sql_with_comment())?;
242        let columns = statement_columns(&statement);
243        let column_names: Arc<[String]> = columns
244            .iter()
245            .map(|column| column.name.clone())
246            .collect::<Vec<_>>()
247            .into();
248        let mut rows = statement.query(params_from_iter(params.iter()))?;
249
250        let mut chunks = Vec::new();
251        let mut current_chunk = Vec::new();
252        let mut chunk_index = 0;
253
254        while let Some(row) = rows.next()? {
255            current_chunk.push(CompactRow::new(
256                column_names.clone(),
257                decode_sqlite_values(row, &columns)?,
258            ));
259            if current_chunk.len() >= chunk_size {
260                chunks.push(teaql_data_service::StreamChunk {
261                    rows: current_chunk,
262                    chunk_index,
263                    is_last: false,
264                });
265                current_chunk = Vec::new();
266                chunk_index += 1;
267            }
268        }
269
270        // Push the final chunk (may be empty if exactly aligned)
271        chunks.push(teaql_data_service::StreamChunk {
272            rows: current_chunk,
273            chunk_index,
274            is_last: true,
275        });
276
277        Ok(chunks)
278    }
279
280    pub fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
281        let exists: i64 = self.lock()?.query_row(
282            "SELECT COUNT(1) FROM sqlite_master WHERE type = 'table' AND name = ?",
283            [table_name],
284            |row| row.get(0),
285        )?;
286        Ok(exists > 0)
287    }
288
289    pub fn table_columns(
290        &self,
291        table_name: &str,
292    ) -> Result<BTreeSet<String>, MutationExecutorError> {
293        let pragma_sql = format!("PRAGMA table_info({})", quote_ident(table_name));
294        let connection = self.lock()?;
295        let mut statement = connection.prepare(&pragma_sql)?;
296        let rows = statement.query_map([], |row| row.get::<_, String>("name"))?;
297        let mut columns = BTreeSet::new();
298        for row in rows {
299            columns.insert(row?.to_lowercase());
300        }
301        Ok(columns)
302    }
303
304    fn lock(&self) -> Result<MutexGuard<'_, Connection>, MutationExecutorError> {
305        self.connection
306            .lock()
307            .map_err(|err| MutationExecutorError::Lock(err.to_string()))
308    }
309}
310
311impl teaql_data_service::DataServiceExecutor for SqliteMutationExecutor {
312    type Error = MutationExecutorError;
313
314    fn capabilities(&self) -> teaql_data_service::DataServiceCapabilities {
315        teaql_data_service::DataServiceCapabilities {
316            query: true,
317            mutation: true,
318            transaction: true,
319            schema: true,
320            id_generation: true,
321            ..Default::default()
322        }
323    }
324}
325
326impl SqlTransport for SqliteMutationExecutor {
327    type Error = MutationExecutorError;
328
329    async fn fetch_all_compact_sql(
330        &self,
331        query: &CompiledQuery,
332    ) -> Result<Vec<CompactRow>, Self::Error> {
333        SqliteMutationExecutor::fetch_all_compact(self, query)
334    }
335
336    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
337        SqliteMutationExecutor::execute(self, query)
338    }
339}
340
341impl teaql_sql::StreamingSqlTransport for SqliteMutationExecutor {
342    // A rusqlite statement and its rows borrow the guarded connection for the
343    // lifetime of the stream. This stream is intentionally local/non-Send, so
344    // retaining the synchronous guard across yields is required and safe.
345    #[allow(clippy::await_holding_lock)]
346    fn stream_sql(
347        &self,
348        query: CompiledQuery,
349        chunk_size: usize,
350    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
351        let connection = self.connection.clone();
352        Box::pin(async_stream::try_stream! {
353            let params = bind_values(&query.params)?;
354            let guard = connection.lock().map_err(|err| MutationExecutorError::Lock(err.to_string()))?;
355            let mut statement = guard.prepare(&query.sql_with_comment())?;
356            let columns = statement_columns(&statement);
357            let column_names: Arc<[String]> = columns.iter().map(|column| column.name.clone()).collect::<Vec<_>>().into();
358            let mut rows = statement.query(params_from_iter(params.iter()))?;
359            let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
360            while let Some(row) = rows.next()? {
361                chunk.push(CompactRow::new(column_names.clone(), decode_sqlite_values(row, &columns)?));
362                if chunk.len() == chunk_size { yield teaql_data_service::StreamChunk { rows: std::mem::take(&mut chunk), chunk_index: index, is_last: false }; index += 1; }
363            }
364            if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows: chunk, chunk_index: index, is_last: true }; }
365        })
366    }
367}
368
369impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
370    fn query_stream(
371        &self,
372        request: teaql_data_service::QueryRequest,
373        chunk_size: usize,
374    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
375        let dialect = SqliteDialect;
376        // Use a dummy entity descriptor for compilation
377        let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
378        match dialect.compile_select(&entity_desc, &request.query) {
379            Ok(compiled) => {
380                teaql_sql::StreamingSqlTransport::stream_sql(self, compiled, chunk_size)
381            }
382            Err(error) => Box::pin(futures_util::stream::once(async {
383                Err(MutationExecutorError::SqlCompile(error))
384            })),
385        }
386    }
387}
388
389impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
390    type Error = MutationExecutorError;
391
392    async fn commit_sql(self) -> Result<(), Self::Error> {
393        self.commit_transaction()
394    }
395
396    async fn rollback_sql(self) -> Result<(), Self::Error> {
397        self.rollback_transaction()
398    }
399}
400
401impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
402    type Tx<'a>
403        = Self
404    where
405        Self: 'a;
406
407    async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
408        self.begin_transaction()?;
409        Ok(self.clone())
410    }
411}
412
413fn initial_graph_exists_sqlite(
414    executor: &SqliteMutationExecutor,
415    dialect: &SqliteDialect,
416    entity: &EntityDescriptor,
417    graph: &GraphNode,
418) -> Result<bool, MutationExecutorError> {
419    let Some(id) = graph.values.get("id") else {
420        return Ok(false);
421    };
422    let query = dialect.compile_select(
423        entity,
424        &SelectQuery::new(&graph.entity)
425            .project("id")
426            .filter(Expr::eq("id", id.clone()))
427            .limit(1),
428    )?;
429    Ok(!executor.fetch_all_compact(&query)?.is_empty())
430}
431
432fn compile_initial_graph_insert(
433    dialect: &impl SqlDialect,
434    entity: &EntityDescriptor,
435    graph: &GraphNode,
436) -> Result<CompiledQuery, MutationExecutorError> {
437    let mut command = InsertCommand::new(&graph.entity);
438    for (field, value) in &graph.values {
439        command = command.value(field.clone(), value.clone());
440    }
441    dialect.compile_insert(entity, &command).map_err(Into::into)
442}
443
444fn compile_initial_graph_update(
445    dialect: &impl SqlDialect,
446    entity: &EntityDescriptor,
447    graph: &GraphNode,
448) -> Result<Option<CompiledQuery>, MutationExecutorError> {
449    let Some(id) = graph.values.get("id") else {
450        return Ok(None);
451    };
452    let mut command = UpdateCommand::new(&graph.entity, id.clone());
453    for (field, value) in &graph.values {
454        if field != "id" {
455            command = command.value(field.clone(), value.clone());
456        }
457    }
458    match dialect.compile_update(entity, &command) {
459        Ok(query) => Ok(Some(query)),
460        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
461        Err(err) => Err(err.into()),
462    }
463}
464
465pub trait SqliteSchemaExt {
466    fn ensure_sqlite_schema(
467        &self,
468    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>>;
469}
470
471pub fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
472    let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
473        MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
474    })?;
475    let executor = context
476        .get_resource::<SqliteMutationExecutor>()
477        .ok_or_else(|| {
478            MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
479        })?;
480
481    let entities = context.all_entities();
482
483    // Ensure id space table exists
484    executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
485
486    // Process each entity table individually with granular events
487    for entity in &entities {
488        let field_count = entity.properties.len();
489        if !executor.table_exists(&entity.table_name)? {
490            // New table: create it
491            let sql = dialect.compile_create_table(entity)?;
492            executor.lock()?.execute(&sql, [])?;
493            let _ = context.send_event(RawAuditEvent::schema_created(
494                &entity.name,
495                &entity.table_name,
496                field_count,
497            ));
498            continue;
499        }
500        // Existing table: check for missing columns
501        let existing_columns = executor.table_columns(&entity.table_name)?;
502        let mut fields_added = 0;
503        for property in &entity.properties {
504            let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
505            if existing_columns.contains(&bare_column) {
506                continue;
507            }
508            let sql = dialect.compile_add_column(entity, property)?;
509            executor.lock()?.execute(&sql, [])?;
510            let _ = context.send_event(RawAuditEvent::field_added(
511                &entity.name,
512                &entity.table_name,
513                &property.column_name,
514            ));
515            fields_added += 1;
516        }
517        let _ = context.send_event(RawAuditEvent::schema_verified(
518            &entity.name,
519            &entity.table_name,
520            field_count,
521        ));
522        let _ = fields_added; // used above for FieldAdded events
523    }
524
525    // Constant graphs are reconciled so model changes are propagated.
526    let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
527    let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); // (inserted, updated)
528    for graph in context.initial_graphs() {
529        let entity = context.entity(&graph.entity).ok_or_else(|| {
530            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
531        })?;
532        let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
533        if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
534            if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
535                executor.execute(&query)?;
536            }
537            counts.1 += 1;
538            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
539                id_generator.ensure_floor(&graph.entity, id)?;
540            }
541            continue;
542        }
543        let query = compile_initial_graph_insert(dialect, entity, graph)?;
544        executor.execute(&query)?;
545        counts.0 += 1; // inserted
546        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
547            id_generator.ensure_floor(&graph.entity, id)?;
548        }
549    }
550
551    // Roots are create-if-absent. Once present, application-owned values win.
552    for graph in context.root_graphs() {
553        let entity = context.entity(&graph.entity).ok_or_else(|| {
554            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
555        })?;
556        if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
557            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
558                id_generator.ensure_floor(&graph.entity, id)?;
559            }
560            continue;
561        }
562        let query = compile_initial_graph_insert(dialect, entity, graph)?;
563        executor.execute(&query)?;
564        seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
565        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
566            id_generator.ensure_floor(&graph.entity, id)?;
567        }
568    }
569
570    // Fire DataSeeded events per entity type
571    for (entity_name, (inserted, updated)) in &seed_counts {
572        let entity = context.entity(entity_name).ok_or_else(|| {
573            MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
574        })?;
575        let _ = context.send_event(RawAuditEvent::data_seeded(
576            entity_name,
577            &entity.table_name,
578            *inserted,
579            *updated,
580        ));
581    }
582
583    Ok(())
584}
585
586impl SqliteSchemaExt for UserContext {
587    fn ensure_sqlite_schema(
588        &self,
589    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>> {
590        Box::pin(async move { ensure_sqlite_schema_for(self) })
591    }
592}
593
594#[derive(Debug, Default, Clone, Copy)]
595pub struct SqliteSchemaProvider;
596
597impl SchemaProvider for SqliteSchemaProvider {
598    fn ensure_schema<'a>(
599        &'a self,
600        context: &'a UserContext,
601    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
602        Box::pin(async move {
603            ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
604        })
605    }
606}
607
608pub trait SqliteProviderExt {
609    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
610}
611
612impl SqliteProviderExt for UserContext {
613    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
614        self.insert_resource(SqliteDialect);
615        self.insert_resource(executor);
616        self.set_schema_provider(SqliteSchemaProvider);
617        self
618    }
619}
620
621#[derive(Clone)]
622pub struct SqliteIdSpaceGenerator {
623    executor: SqliteMutationExecutor,
624    table_name: String,
625}
626
627impl SqliteIdSpaceGenerator {
628    pub fn new(connection: Connection) -> Self {
629        Self::from_executor(SqliteMutationExecutor::from_connection(connection))
630    }
631
632    pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
633        Self {
634            executor,
635            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
636        }
637    }
638
639    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
640        self.table_name = table_name.into();
641        self
642    }
643
644    pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
645        self.executor.ensure_id_space_table(&self.table_name)
646    }
647
648    pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
649        let entity = canonical_id_space_entity(entity);
650        let entity = entity.as_str();
651        self.ensure_table()?;
652        let table = quote_ident(&self.table_name);
653        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
654        let insert_sql = format!(
655            "INSERT INTO {table} (type_name, current_level) VALUES (?, 1)"
656        );
657        let update_sql = format!(
658            "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
659        );
660        for attempt in 1..=100 {
661            let connection = self.executor.lock()?;
662            let current = connection
663                .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
664                .optional()?;
665            if let Some(current) = current {
666                let next = current.checked_add(1).ok_or_else(|| {
667                    MutationExecutorError::Bind(format!(
668                        "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
669                    ))
670                })?;
671                if connection.execute(&update_sql, params![next, entity, current])? == 1 {
672                    return u64::try_from(next).map_err(|_| {
673                        MutationExecutorError::Bind(format!(
674                            "generated id {next} cannot be represented as u64"
675                        ))
676                    });
677                }
678            } else {
679                match connection.execute(&insert_sql, params![entity]) {
680                    Ok(1) => return Ok(1),
681                    Ok(changed) => {
682                        return Err(MutationExecutorError::Bind(format!(
683                            "ID space insert for {entity} changed {changed} rows"
684                        )))
685                    }
686                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
687                    Err(error) => return Err(error.into()),
688                }
689            }
690        }
691        Err(MutationExecutorError::Bind(format!(
692            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
693        )))
694    }
695
696    pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
697        let entity = canonical_id_space_entity(entity);
698        let entity = entity.as_str();
699        self.ensure_table()?;
700        let floor = i64::try_from(floor).map_err(|_| {
701            MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
702        })?;
703        let table = quote_ident(&self.table_name);
704        for _ in 1..=100 {
705            let connection = self.executor.lock()?;
706            let current = connection
707                .query_row(
708                    &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
709                    [entity],
710                    |row| row.get::<_, i64>(0),
711                )
712                .optional()?;
713            match current {
714                Some(current) if current >= floor => return Ok(()),
715                Some(current) => {
716                    if connection.execute(
717                        &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
718                        params![floor, entity, current],
719                    )? == 1 { return Ok(()); }
720                }
721                None => match connection.execute(
722                    &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
723                    params![entity, floor],
724                ) {
725                    Ok(1) => return Ok(()),
726                    Ok(_) => {}
727                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
728                    Err(error) => return Err(error.into()),
729                },
730            }
731        }
732        Err(MutationExecutorError::Bind(format!(
733            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
734        )))
735    }
736}
737
738impl InternalIdGenerator for SqliteIdSpaceGenerator {
739    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
740        self.next_id(entity)
741            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
742    }
743}
744
745fn quote_ident(ident: &str) -> String {
746    quote_identifier_if_needed(ident, '"')
747}
748
749/// Strip wrapping identifier quotes from a SQL identifier.
750///
751/// SQLite `PRAGMA table_info` returns bare column names (e.g. `description`),
752/// but generated `PropertyDescriptor::column_name` may carry quotes
753/// (e.g. `"description"`) when the name is a reserved keyword.  This helper
754/// normalises the column name so the two can be compared correctly during
755/// schema migration.
756fn strip_identifier_quotes(ident: &str) -> &str {
757    let bytes = ident.as_bytes();
758    if bytes.len() >= 2 {
759        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
760        if (first == b'"' && last == b'"')
761            || (first == b'`' && last == b'`')
762            || (first == b'[' && last == b']')
763        {
764            return &ident[1..ident.len() - 1];
765        }
766    }
767    ident
768}
769
770fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
771    values.iter().map(bind_sqlite_value).collect()
772}
773
774fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
775    match value {
776        Value::Null => Ok(SqliteValue::Null),
777        Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
778        Value::I64(v) => Ok(SqliteValue::Integer(*v)),
779        Value::U64(v) => i64::try_from(*v)
780            .map(SqliteValue::Integer)
781            .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
782        Value::F64(v) => Ok(SqliteValue::Real(*v)),
783        // Bind the canonical numeric spelling. SQLite NUMERIC affinity keeps
784        // predicates and aggregates numeric; an application-only text prefix
785        // makes range comparisons silently return the wrong result.
786        Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
787        Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
788        Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
789        Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
790        Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
791        Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
792        Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
793        Value::TypedNull(_) => Ok(SqliteValue::Null),
794    }
795}
796
797#[derive(Debug, Clone)]
798struct ColumnInfo {
799    name: String,
800    decl_type: Option<String>,
801}
802
803fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
804    statement
805        .columns()
806        .into_iter()
807        .map(|column| ColumnInfo {
808            name: column.name().to_owned(),
809            decl_type: column.decl_type().map(|value| value.to_ascii_uppercase()),
810        })
811        .collect()
812}
813
814fn decode_sqlite_values(
815    row: &Row<'_>,
816    columns: &[ColumnInfo],
817) -> Result<Vec<Value>, MutationExecutorError> {
818    let mut values = Vec::with_capacity(columns.len());
819    for (index, column) in columns.iter().enumerate() {
820        let value_ref = row.get_ref(index)?;
821        let value = match value_ref {
822            ValueRef::Null => Value::Null,
823            ValueRef::Integer(value) => decode_sqlite_integer(value, column),
824            ValueRef::Real(value) => Value::F64(value),
825            ValueRef::Text(value) => decode_sqlite_text(value, column)?,
826            ValueRef::Blob(_) => {
827                return Err(MutationExecutorError::UnsupportedColumnType(
828                    "BLOB".to_owned(),
829                ));
830            }
831        };
832        values.push(value);
833    }
834    Ok(values)
835}
836
837fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
838    match column_decl_type(column).as_deref() {
839        Some("BOOLEAN") | Some("BOOL") => Value::Bool(value != 0),
840        _ => Value::I64(value),
841    }
842}
843
844fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
845    let value = std::str::from_utf8(value)
846        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
847    match column_decl_type(column).as_deref() {
848        Some("NUMERIC") | Some("DECIMAL") => Decimal::from_str(value)
849            .map(Value::Decimal)
850            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
851        Some("JSON") => serde_json::from_str(value).map(Value::Json).map_err(|err| {
852            MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
853        }),
854        Some("DATE") => NaiveDate::parse_from_str(value, "%Y-%m-%d")
855            .map(Value::Date)
856            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
857        Some("TIMESTAMP") | Some("DATETIME") => parse_sqlite_timestamp(value),
858        _ => infer_sqlite_text(value),
859    }
860}
861
862fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
863    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
864        return Ok(Value::Date(date));
865    }
866    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
867        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
868            timestamp.timestamp_millis(),
869        )));
870    }
871    if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
872        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
873            timestamp.and_utc().timestamp_millis(),
874        )));
875    }
876    Ok(Value::Text(value.to_owned()))
877}
878
879fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
880    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
881        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
882            timestamp.timestamp_millis(),
883        )));
884    }
885    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
886        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
887            date.and_hms_opt(0, 0, 0)
888                .unwrap_or_default()
889                .and_utc()
890                .timestamp_millis(),
891        )));
892    }
893    NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
894        .map(|timestamp| {
895            Value::Timestamp(teaql_core::time::Timestamp(
896                timestamp.and_utc().timestamp_millis(),
897            ))
898        })
899        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
900}
901
902fn column_decl_type(column: &ColumnInfo) -> Option<String> {
903    column
904        .decl_type
905        .as_ref()
906        .map(|value| value.split('(').next().unwrap_or(value).trim().to_owned())
907}
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912    use futures_util::StreamExt;
913    use teaql_core::{DeleteCommand, Record, RecoverCommand};
914    use teaql_macros::TeaqlEntity;
915    use teaql_runtime::InMemoryMetadataStore;
916
917    #[test]
918    fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
919        let connection = Connection::open_in_memory().unwrap();
920        connection
921            .execute_batch(
922                "CREATE TABLE stream_fixture(id INTEGER);\
923                 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
924            )
925            .unwrap();
926        let executor = SqliteMutationExecutor::from_connection(connection);
927        let query = CompiledQuery {
928            sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
929            params: vec![],
930            comment: None,
931        };
932        let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
933        let sizes = futures_executor::block_on(async {
934            let mut result = Vec::new();
935            while let Some(chunk) = stream.next().await {
936                result.push(chunk.unwrap().rows.len());
937            }
938            result
939        });
940        assert_eq!(sizes, vec![2, 2, 1]);
941
942        let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
943        assert_eq!(
944            futures_executor::block_on(early.next())
945                .unwrap()
946                .unwrap()
947                .rows
948                .len(),
949            2
950        );
951        drop(early);
952        let count: i64 = executor
953            .connection()
954            .lock()
955            .unwrap()
956            .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
957            .unwrap();
958        assert_eq!(count, 5);
959    }
960
961    #[test]
962    fn decimal_bind_is_numeric_and_comparable() {
963        let value =
964            bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
965        assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
966        let connection = Connection::open_in_memory().unwrap();
967        let matches: i64 = connection
968            .query_row(
969                "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
970                [value],
971                |row| row.get(0),
972            )
973            .unwrap();
974        assert_eq!(matches, 1);
975    }
976
977    #[test]
978    fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
979        let connection = Connection::open_in_memory().unwrap();
980        connection
981            .execute_batch(
982                "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
983            )
984            .unwrap();
985        let query = CompiledQuery {
986            sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
987            params: vec![
988                Value::I64(1),
989                Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
990                Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
991            ],
992            comment: None,
993        };
994        let values = bind_values(&query.params).unwrap();
995        connection
996            .execute(&query.sql, rusqlite::params_from_iter(values))
997            .unwrap();
998        connection
999            .execute(
1000                &query
1001                    .debug_sql(teaql_sql::DatabaseKind::Sqlite)
1002                    .replace("VALUES (1,", "VALUES (2,"),
1003                [],
1004            )
1005            .unwrap();
1006
1007        let equal_count: i64 = connection.query_row(
1008            "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",
1009            [], |row| row.get(0),
1010        ).unwrap();
1011        let storage_type: String = connection
1012            .query_row(
1013                "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
1014                [],
1015                |row| row.get(0),
1016            )
1017            .unwrap();
1018        assert_eq!(equal_count, 1);
1019        assert_eq!(storage_type, "integer");
1020    }
1021
1022    fn entity() -> EntityDescriptor {
1023        EntityDescriptor::new("Order")
1024            .table_name("orders")
1025            .property(
1026                PropertyDescriptor::new("id", DataType::U64)
1027                    .column_name("id")
1028                    .id()
1029                    .not_null(),
1030            )
1031            .property(
1032                PropertyDescriptor::new("version", DataType::I64)
1033                    .column_name("version")
1034                    .version()
1035                    .not_null(),
1036            )
1037            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1038    }
1039
1040    fn order_line_entity() -> EntityDescriptor {
1041        EntityDescriptor::new("OrderLine")
1042            .table_name("order_line")
1043            .property(
1044                PropertyDescriptor::new("id", DataType::U64)
1045                    .column_name("id")
1046                    .id()
1047                    .not_null(),
1048            )
1049            .property(
1050                PropertyDescriptor::new("order_id", DataType::U64)
1051                    .column_name("order_id")
1052                    .not_null(),
1053            )
1054            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1055    }
1056
1057    #[allow(dead_code)]
1058    #[derive(Debug, PartialEq, TeaqlEntity)]
1059    #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1060    struct FeatureFlagRow {
1061        #[teaql(id)]
1062        id: u64,
1063        #[teaql(version)]
1064        version: i64,
1065        enabled: bool,
1066        optional_enabled: Option<bool>,
1067    }
1068
1069    fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1070        Record::from([
1071            ("id".to_owned(), Value::U64(1)),
1072            ("version".to_owned(), Value::I64(1)),
1073            ("enabled".to_owned(), enabled),
1074            ("optional_enabled".to_owned(), optional_enabled),
1075        ])
1076    }
1077
1078    #[test]
1079    fn sqlite_dialect_compiles_mutations_and_schema() {
1080        let insert = SqliteDialect
1081            .compile_insert(
1082                &entity(),
1083                &InsertCommand::new("Order")
1084                    .value("id", 1_u64)
1085                    .value("name", "A"),
1086            )
1087            .unwrap();
1088        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1089
1090        let update = SqliteDialect
1091            .compile_update(
1092                &entity(),
1093                &UpdateCommand::new("Order", 1_u64)
1094                    .expected_version(3)
1095                    .value("name", "B"),
1096            )
1097            .unwrap();
1098        assert_eq!(
1099            update.sql,
1100            "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1101        );
1102
1103        let delete = SqliteDialect
1104            .compile_delete(
1105                &entity(),
1106                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1107            )
1108            .unwrap();
1109        let recover = SqliteDialect
1110            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1111            .unwrap();
1112        assert_eq!(
1113            delete.sql,
1114            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1115        );
1116        assert_eq!(
1117            recover.sql,
1118            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1119        );
1120
1121        let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1122        assert_eq!(
1123            create,
1124            "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1125        );
1126    }
1127
1128    #[test]
1129    fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1130        let executor =
1131            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1132        let entity = entity();
1133        let mut context = UserContext::new()
1134            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1135
1136        context.use_sqlite_provider(executor.clone());
1137        ensure_sqlite_schema_for(&context).unwrap();
1138
1139        let insert = SqliteDialect
1140            .compile_insert(
1141                &entity,
1142                &InsertCommand::new("Order")
1143                    .value("id", 1_u64)
1144                    .value("version", 1_i64)
1145                    .value("name", "draft"),
1146            )
1147            .unwrap();
1148        assert_eq!(executor.execute(&insert).unwrap(), 1);
1149
1150        let select = SqliteDialect
1151            .compile_select(
1152                &entity,
1153                &SelectQuery::new("Order")
1154                    .filter(Expr::eq("id", 1_u64))
1155                    .order_asc("id"),
1156            )
1157            .unwrap();
1158        let rows = executor.fetch_all_compact(&select).unwrap();
1159        assert_eq!(rows.len(), 1);
1160        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1161        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1162        assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
1163    }
1164
1165    #[test]
1166    fn repeated_schema_ensure_does_not_overwrite_existing_initial_graph() {
1167        let executor =
1168            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1169        let entity = entity();
1170        let mut context = UserContext::new()
1171            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1172        context.set_root_graphs(vec![
1173            GraphNode::new("Order")
1174                .value("id", 1_u64)
1175                .value("version", 1_i64)
1176                .value("name", "module seed"),
1177        ]);
1178        context.use_sqlite_provider(executor.clone());
1179
1180        ensure_sqlite_schema_for(&context).unwrap();
1181        let customize = SqliteDialect
1182            .compile_update(
1183                &entity,
1184                &UpdateCommand::new("Order", 1_u64).value("name", "application value"),
1185            )
1186            .unwrap();
1187        assert_eq!(executor.execute(&customize).unwrap(), 1);
1188
1189        ensure_sqlite_schema_for(&context).unwrap();
1190
1191        let select = SqliteDialect
1192            .compile_select(
1193                &entity,
1194                &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1195            )
1196            .unwrap();
1197        let rows = executor.fetch_all_compact(&select).unwrap();
1198        assert_eq!(rows.len(), 1);
1199        assert_eq!(
1200            rows[0].get("name"),
1201            Some(&Value::Text("application value".to_owned()))
1202        );
1203    }
1204
1205    #[test]
1206    fn repeated_schema_ensure_reconciles_changed_constant_graph() {
1207        let executor =
1208            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1209        let entity = entity();
1210        let mut context = UserContext::new()
1211            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1212        context.set_initial_graphs(vec![
1213            GraphNode::new("Order")
1214                .value("id", 1001_u64)
1215                .value("version", 1_i64)
1216                .value("name", "red"),
1217        ]);
1218        context.use_sqlite_provider(executor.clone());
1219        ensure_sqlite_schema_for(&context).unwrap();
1220
1221        context.set_initial_graphs(vec![
1222            GraphNode::new("Order")
1223                .value("id", 1001_u64)
1224                .value("version", 1_i64)
1225                .value("name", "crimson"),
1226        ]);
1227        ensure_sqlite_schema_for(&context).unwrap();
1228
1229        let select = SqliteDialect
1230            .compile_select(
1231                &entity,
1232                &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1233            )
1234            .unwrap();
1235        let rows = executor.fetch_all_compact(&select).unwrap();
1236        assert_eq!(
1237            rows[0].get("name"),
1238            Some(&Value::Text("crimson".to_owned()))
1239        );
1240        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1241        assert_eq!(generator.next_id("Order").unwrap(), 1002);
1242    }
1243
1244    #[test]
1245    fn sqlite_executes_partitioned_relation_limit_per_parent() {
1246        let executor =
1247            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1248        let entity = order_line_entity();
1249        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1250
1251        for order_id in [11_u64, 12_u64] {
1252            for index in 1_u64..=5 {
1253                let id = order_id * 100 + index;
1254                let insert = SqliteDialect
1255                    .compile_insert(
1256                        &entity,
1257                        &InsertCommand::new("OrderLine")
1258                            .value("id", id)
1259                            .value("order_id", order_id)
1260                            .value("name", format!("line-{id}")),
1261                    )
1262                    .unwrap();
1263                executor.execute(&insert).unwrap();
1264            }
1265        }
1266
1267        let query = SelectQuery::new("OrderLine")
1268            .project("id")
1269            .project("order_id")
1270            .order_desc("id")
1271            .limit(3)
1272            .partition_by("order_id");
1273        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1274        let rows = executor.fetch_all_compact(&compiled).unwrap();
1275
1276        assert_eq!(rows.len(), 6);
1277        for order_id in [11_i64, 12_i64] {
1278            let ids = rows
1279                .iter()
1280                .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
1281                .filter_map(|row| row.get("id").cloned())
1282                .collect::<Vec<_>>();
1283            assert_eq!(
1284                ids,
1285                vec![
1286                    Value::I64(order_id * 100 + 5),
1287                    Value::I64(order_id * 100 + 4),
1288                    Value::I64(order_id * 100 + 3),
1289                ]
1290            );
1291        }
1292    }
1293
1294    #[test]
1295    fn sqlite_boolean_new_schema_roundtrips_as_bool() {
1296        let executor =
1297            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1298        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1299        let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
1300        assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
1301        assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
1302        assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
1303
1304        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1305        for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
1306            let insert = SqliteDialect
1307                .compile_insert(
1308                    &entity,
1309                    &InsertCommand::new("FeatureFlag")
1310                        .value("id", id)
1311                        .value("version", 1_i64)
1312                        .value("enabled", enabled)
1313                        .value("optional_enabled", optional_enabled),
1314                )
1315                .unwrap();
1316            assert_eq!(executor.execute(&insert).unwrap(), 1);
1317        }
1318
1319        let select = SqliteDialect
1320            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1321            .unwrap();
1322        let rows = executor.fetch_all_compact(&select).unwrap();
1323        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
1324        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
1325        assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
1326        assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
1327
1328        let first = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
1329        let second = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap();
1330        assert!(!first.enabled);
1331        assert_eq!(first.optional_enabled, Some(true));
1332        assert!(second.enabled);
1333        assert_eq!(second.optional_enabled, Some(false));
1334    }
1335
1336    #[test]
1337    fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
1338        let executor =
1339            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1340        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1341        executor
1342            .execute(&CompiledQuery {
1343                sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
1344                    .to_owned(),
1345                params: Vec::new(),
1346                comment: None,
1347            })
1348            .unwrap();
1349
1350        let insert = SqliteDialect
1351            .compile_insert(
1352                &entity,
1353                &InsertCommand::new("FeatureFlag")
1354                    .value("id", 1_u64)
1355                    .value("version", 1_i64)
1356                    .value("enabled", true)
1357                    .value("optional_enabled", false),
1358            )
1359            .unwrap();
1360        executor.execute(&insert).unwrap();
1361        executor
1362            .execute(&CompiledQuery {
1363                sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
1364                    .to_owned(),
1365                params: vec![
1366                    Value::U64(2),
1367                    Value::I64(1),
1368                    Value::I64(2),
1369                    Value::Null,
1370                ],
1371                comment: None,
1372            })
1373            .unwrap();
1374        let select = SqliteDialect
1375            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1376            .unwrap();
1377        let rows = executor.fetch_all_compact(&select).unwrap();
1378        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1379        assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
1380        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
1381
1382        let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
1383        assert!(decoded.enabled);
1384        assert_eq!(decoded.optional_enabled, Some(false));
1385        assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
1386        let error =
1387            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap_err();
1388        assert!(error.message.contains("invalid field enabled"));
1389
1390        for (value, expected) in [
1391            (Value::I64(0), false),
1392            (Value::I64(1), true),
1393            (Value::U64(0), false),
1394            (Value::U64(1), true),
1395        ] {
1396            let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
1397                teaql_core::CompactRow::from_map(feature_flag_record(value, Value::Null)),
1398            )
1399            .unwrap();
1400            assert_eq!(decoded.enabled, expected);
1401            assert_eq!(decoded.optional_enabled, None);
1402        }
1403
1404        for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
1405            let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
1406                teaql_core::CompactRow::from_map(feature_flag_record(invalid, Value::Null)),
1407            )
1408            .unwrap_err();
1409            assert!(error.message.contains("invalid field enabled"));
1410        }
1411        let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
1412            teaql_core::CompactRow::from_map(feature_flag_record(
1413                Value::Bool(true),
1414                Value::U64(2),
1415            )),
1416        )
1417        .unwrap_err();
1418        assert!(error.message.contains("invalid field optional_enabled"));
1419    }
1420
1421    #[test]
1422    fn sqlite_executor_parses_json_only_for_json_columns() {
1423        let executor =
1424            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1425
1426        executor
1427            .execute(&CompiledQuery {
1428                sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
1429                params: Vec::new(),
1430                comment: None,
1431            })
1432            .unwrap();
1433        executor
1434            .execute(&CompiledQuery {
1435                sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
1436                params: vec![
1437                    Value::Text("{\"active\":true}".to_owned()),
1438                    Value::Json(serde_json::json!({"active": true})),
1439                ],
1440                comment: None,
1441            })
1442            .unwrap();
1443
1444        let rows = executor
1445            .fetch_all_compact(&CompiledQuery {
1446                sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
1447                params: Vec::new(),
1448                comment: None,
1449            })
1450            .unwrap();
1451
1452        assert_eq!(
1453            rows[0].get("text_payload"),
1454            Some(&Value::Text("{\"active\":true}".to_owned()))
1455        );
1456        assert_eq!(
1457            rows[0].get("json_payload"),
1458            Some(&Value::Json(serde_json::json!({"active": true})))
1459        );
1460    }
1461
1462    #[test]
1463    fn sqlite_id_space_generator_increments_ids() {
1464        let executor =
1465            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1466        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1467        assert_eq!(generator.next_id("Order").unwrap(), 1);
1468        assert_eq!(generator.next_id("Order").unwrap(), 2);
1469    }
1470
1471    #[test]
1472    fn sqlite_id_space_generator_is_safe_across_connections() {
1473        let path = std::env::temp_dir().join(format!(
1474            "teaql-id-space-{}-{}.db",
1475            std::process::id(),
1476            std::time::SystemTime::now()
1477                .duration_since(std::time::UNIX_EPOCH)
1478                .unwrap()
1479                .as_nanos()
1480        ));
1481        let mut workers = Vec::new();
1482        for _ in 0..4 {
1483            let path = path.clone();
1484            workers.push(std::thread::spawn(move || {
1485                let connection = Connection::open(path).unwrap();
1486                connection
1487                    .busy_timeout(std::time::Duration::from_secs(5))
1488                    .unwrap();
1489                let generator = SqliteIdSpaceGenerator::new(connection);
1490                (0..25)
1491                    .map(|_| generator.next_id("Order").unwrap())
1492                    .collect::<Vec<_>>()
1493            }));
1494        }
1495        let mut ids = workers
1496            .into_iter()
1497            .flat_map(|worker| worker.join().unwrap())
1498            .collect::<Vec<_>>();
1499        ids.sort_unstable();
1500        assert_eq!(ids, (1..=100).collect::<Vec<_>>());
1501        let _ = std::fs::remove_file(path);
1502    }
1503
1504    #[test]
1505    fn sqlite_fetch_stream_returns_chunked_rows() {
1506        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1507            Connection::open_in_memory().unwrap(),
1508        )));
1509        let entity = entity();
1510
1511        // Create table and insert 25 rows
1512        executor
1513            .execute(&CompiledQuery {
1514                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1515                    .to_owned(),
1516                params: Vec::new(),
1517                comment: None,
1518            })
1519            .unwrap();
1520
1521        for i in 1..=25 {
1522            let insert = SqliteDialect
1523                .compile_insert(
1524                    &entity,
1525                    &InsertCommand::new("Order")
1526                        .value("id", i as u64)
1527                        .value("version", 1_i64)
1528                        .value("name", format!("order-{i}")),
1529                )
1530                .unwrap();
1531            executor.execute(&insert).unwrap();
1532        }
1533
1534        // Stream with chunk_size = 10
1535        let query = SelectQuery::new("Order")
1536            .filter(Expr::gt("version", 0_i64))
1537            .order_asc("id")
1538            .stream(10);
1539
1540        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1541
1542        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1543
1544        // 25 rows / 10 per chunk = 3 chunks
1545        assert_eq!(chunks.len(), 3);
1546        assert_eq!(chunks[0].rows.len(), 10);
1547        assert_eq!(chunks[0].chunk_index, 0);
1548        assert!(!chunks[0].is_last);
1549
1550        assert_eq!(chunks[1].rows.len(), 10);
1551        assert_eq!(chunks[1].chunk_index, 1);
1552        assert!(!chunks[1].is_last);
1553
1554        assert_eq!(chunks[2].rows.len(), 5);
1555        assert_eq!(chunks[2].chunk_index, 2);
1556        assert!(chunks[2].is_last);
1557
1558        // Verify first and last row
1559        assert_eq!(
1560            chunks[0].rows[0].get("name"),
1561            Some(&Value::Text("order-1".to_owned()))
1562        );
1563        assert_eq!(
1564            chunks[2].rows[4].get("name"),
1565            Some(&Value::Text("order-25".to_owned()))
1566        );
1567    }
1568
1569    #[test]
1570    fn sqlite_fetch_stream_handles_empty_result() {
1571        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1572            Connection::open_in_memory().unwrap(),
1573        )));
1574
1575        executor
1576            .execute(&CompiledQuery {
1577                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1578                    .to_owned(),
1579                params: Vec::new(),
1580                comment: None,
1581            })
1582            .unwrap();
1583
1584        let entity = entity();
1585        let query = SelectQuery::new("Order")
1586            .filter(Expr::gt("version", 0_i64))
1587            .stream(10);
1588
1589        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1590
1591        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1592
1593        // Empty result = 1 chunk with 0 rows, marked as last
1594        assert_eq!(chunks.len(), 1);
1595        assert_eq!(chunks[0].rows.len(), 0);
1596        assert!(chunks[0].is_last);
1597    }
1598
1599    #[test]
1600    fn sqlite_fetch_stream_exact_chunk_boundary() {
1601        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1602            Connection::open_in_memory().unwrap(),
1603        )));
1604        let entity = entity();
1605
1606        executor
1607            .execute(&CompiledQuery {
1608                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1609                    .to_owned(),
1610                params: Vec::new(),
1611                comment: None,
1612            })
1613            .unwrap();
1614
1615        // Insert exactly 20 rows
1616        for i in 1..=20 {
1617            let insert = SqliteDialect
1618                .compile_insert(
1619                    &entity,
1620                    &InsertCommand::new("Order")
1621                        .value("id", i as u64)
1622                        .value("version", 1_i64)
1623                        .value("name", format!("order-{i}")),
1624                )
1625                .unwrap();
1626            executor.execute(&insert).unwrap();
1627        }
1628
1629        let query = SelectQuery::new("Order")
1630            .filter(Expr::gt("version", 0_i64))
1631            .order_asc("id")
1632            .stream(10);
1633
1634        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1635
1636        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1637
1638        // 20 rows / 10 per chunk = 2 full chunks + 1 empty final chunk
1639        assert_eq!(chunks.len(), 3);
1640        assert_eq!(chunks[0].rows.len(), 10);
1641        assert!(!chunks[0].is_last);
1642        assert_eq!(chunks[1].rows.len(), 10);
1643        assert!(!chunks[1].is_last);
1644        assert_eq!(chunks[2].rows.len(), 0);
1645        assert!(chunks[2].is_last);
1646    }
1647
1648    #[test]
1649    fn test_parse_sqlite_timestamp() {
1650        let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
1651        assert!(matches!(ts1, Value::Timestamp(_)));
1652
1653        let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
1654        assert!(matches!(ts2, Value::Timestamp(_)));
1655
1656        let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
1657        assert!(matches!(ts3, Value::Timestamp(_)));
1658
1659        assert!(parse_sqlite_timestamp("invalid").is_err());
1660    }
1661}