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, Row, params_from_iter};
10use rust_decimal::Decimal;
11use teaql_core::{
12 DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, Record, SelectQuery,
13 UpdateCommand, Value,
14};
15use teaql_runtime::{
16 GraphNode, InternalIdGenerator, RawAuditEvent, RuntimeError, SchemaProvider, UserContext,
17};
18use teaql_sql::{
19 CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
20 quote_identifier_if_needed,
21};
22
23pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
24
25const SQLITE_DECIMAL_PREFIX: &str = "__teaql_decimal__:";
26
27#[derive(Debug, Default, Clone, Copy)]
28pub struct SqliteDialect;
29
30impl SqlDialect for SqliteDialect {
31 fn kind(&self) -> DatabaseKind {
32 DatabaseKind::Sqlite
33 }
34
35 fn quote_ident(&self, ident: &str) -> String {
36 quote_ident(ident)
37 }
38
39 fn placeholder(&self, _index: usize) -> String {
40 "?".to_owned()
41 }
42
43 fn schema_type_sql(
44 &self,
45 data_type: DataType,
46 property: &PropertyDescriptor,
47 ) -> Result<&'static str, SqlCompileError> {
48 match data_type {
49 DataType::Bool => Ok("BOOLEAN"),
50 DataType::I64 | DataType::U64 if property.is_id => Ok("INTEGER"),
51 DataType::I64 | DataType::U64 => Ok("INTEGER"),
52 DataType::F64 => Ok("REAL"),
53 DataType::Decimal => Ok("NUMERIC"),
54 DataType::Text => Ok("VARCHAR(255)"),
55 DataType::LargeText => Ok("TEXT"),
56 DataType::Json => Ok("JSON"),
57 DataType::Date => Ok("DATE"),
58 DataType::Timestamp => Ok("TIMESTAMP"),
59 }
60 }
61
62 fn compile_add_column(
63 &self,
64 entity: &EntityDescriptor,
65 property: &PropertyDescriptor,
66 ) -> Result<String, SqlCompileError> {
67 let def = self.column_definition_sql(property)?;
71 let def_without_not_null = def.replace(" NOT NULL", "");
72
73 Ok(format!(
74 "ALTER TABLE {} ADD COLUMN {}",
75 self.quote_ident(&entity.table_name),
76 def_without_not_null
77 ))
78 }
79}
80
81#[derive(Debug)]
82pub enum MutationExecutorError {
83 Sqlite(rusqlite::Error),
84 SqlCompile(SqlCompileError),
85 UnsupportedValue(&'static str),
86 UnsupportedColumnType(String),
87 Bind(String),
88 Lock(String),
89}
90
91impl std::fmt::Display for MutationExecutorError {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::Sqlite(err) => err.fmt(f),
95 Self::SqlCompile(err) => err.fmt(f),
96 Self::UnsupportedValue(kind) => {
97 write!(
98 f,
99 "unsupported rusqlite bind value for mutation executor: {kind}"
100 )
101 }
102 Self::UnsupportedColumnType(kind) => {
103 write!(
104 f,
105 "unsupported rusqlite column type for record decoding: {kind}"
106 )
107 }
108 Self::Bind(message) => write!(f, "rusqlite bind error: {message}"),
109 Self::Lock(message) => write!(f, "rusqlite connection lock error: {message}"),
110 }
111 }
112}
113
114impl std::error::Error for MutationExecutorError {}
115
116impl From<rusqlite::Error> for MutationExecutorError {
117 fn from(value: rusqlite::Error) -> Self {
118 Self::Sqlite(value)
119 }
120}
121
122impl From<SqlCompileError> for MutationExecutorError {
123 fn from(value: SqlCompileError) -> Self {
124 Self::SqlCompile(value)
125 }
126}
127
128#[derive(Clone)]
129pub struct SqliteMutationExecutor {
130 connection: Arc<Mutex<Connection>>,
131}
132
133impl SqliteMutationExecutor {
134 pub fn new(connection: Arc<Mutex<Connection>>) -> Self {
135 Self { connection }
136 }
137
138 pub fn from_connection(connection: Connection) -> Self {
139 Self::new(Arc::new(Mutex::new(connection)))
140 }
141
142 pub fn connection(&self) -> Arc<Mutex<Connection>> {
143 Arc::clone(&self.connection)
144 }
145
146 pub fn ensure_schema(
147 &self,
148 dialect: &SqliteDialect,
149 entities: &[&EntityDescriptor],
150 ) -> Result<(), MutationExecutorError> {
151 self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
152
153 for entity in entities {
154 if !self.table_exists(&entity.table_name)? {
155 let sql = dialect.compile_create_table(entity)?;
156 self.lock()?.execute(&sql, [])?;
157 continue;
158 }
159
160 let existing_columns = self.table_columns(&entity.table_name)?;
161 for property in &entity.properties {
162 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
163 if existing_columns.contains(&bare_column) {
164 continue;
165 }
166 let sql = dialect.compile_add_column(entity, property)?;
167 self.lock()?.execute(&sql, [])?;
168 }
169
170 for sql in dialect.schema_indexes_sqls(entity)? {
171 self.lock()?.execute(&sql, [])?;
172 }
173 }
174 Ok(())
175 }
176
177 pub fn ensure_id_space_table(&self, table_name: &str) -> Result<(), MutationExecutorError> {
178 let sql = format!(
179 "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
180 quote_ident(table_name)
181 );
182 self.lock()?.execute(&sql, [])?;
183 Ok(())
184 }
185
186 pub fn begin_transaction(&self) -> Result<(), MutationExecutorError> {
187 self.lock()?.execute("BEGIN IMMEDIATE", [])?;
188 Ok(())
189 }
190
191 pub fn commit_transaction(&self) -> Result<(), MutationExecutorError> {
192 self.lock()?.execute("COMMIT", [])?;
193 Ok(())
194 }
195
196 pub fn rollback_transaction(&self) -> Result<(), MutationExecutorError> {
197 self.lock()?.execute("ROLLBACK", [])?;
198 Ok(())
199 }
200
201 pub fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
202 let params = bind_values(&query.params)?;
203 let rows = self
204 .lock()?
205 .execute(&query.sql_with_comment(), params_from_iter(params.iter()))?;
206 Ok(rows as u64)
207 }
208
209 pub fn fetch_all(&self, query: &CompiledQuery) -> Result<Vec<Record>, MutationExecutorError> {
210 let params = bind_values(&query.params)?;
211 let connection = self.lock()?;
212 let mut statement = connection.prepare(&query.sql_with_comment())?;
213 let columns = statement_columns(&statement);
214 let mut rows = statement.query(params_from_iter(params.iter()))?;
215 let mut records = Vec::new();
216 while let Some(row) = rows.next()? {
217 records.push(decode_sqlite_row(row, &columns)?);
218 }
219 Ok(records)
220 }
221
222 pub fn fetch_stream(
225 &self,
226 query: &CompiledQuery,
227 chunk_size: usize,
228 ) -> Result<Vec<teaql_data_service::StreamChunk>, MutationExecutorError> {
229 let params = bind_values(&query.params)?;
230 let connection = self.lock()?;
231 let mut statement = connection.prepare(&query.sql_with_comment())?;
232 let columns = statement_columns(&statement);
233 let mut rows = statement.query(params_from_iter(params.iter()))?;
234
235 let mut chunks = Vec::new();
236 let mut current_chunk = Vec::new();
237 let mut chunk_index = 0;
238
239 while let Some(row) = rows.next()? {
240 current_chunk.push(decode_sqlite_row(row, &columns)?);
241 if current_chunk.len() >= chunk_size {
242 chunks.push(teaql_data_service::StreamChunk {
243 rows: current_chunk,
244 chunk_index,
245 is_last: false,
246 });
247 current_chunk = Vec::new();
248 chunk_index += 1;
249 }
250 }
251
252 chunks.push(teaql_data_service::StreamChunk {
254 rows: current_chunk,
255 chunk_index,
256 is_last: true,
257 });
258
259 Ok(chunks)
260 }
261
262 pub fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
263 let exists: i64 = self.lock()?.query_row(
264 "SELECT COUNT(1) FROM sqlite_master WHERE type = 'table' AND name = ?",
265 [table_name],
266 |row| row.get(0),
267 )?;
268 Ok(exists > 0)
269 }
270
271 pub fn table_columns(
272 &self,
273 table_name: &str,
274 ) -> Result<BTreeSet<String>, MutationExecutorError> {
275 let pragma_sql = format!("PRAGMA table_info({})", quote_ident(table_name));
276 let connection = self.lock()?;
277 let mut statement = connection.prepare(&pragma_sql)?;
278 let rows = statement.query_map([], |row| row.get::<_, String>("name"))?;
279 let mut columns = BTreeSet::new();
280 for row in rows {
281 columns.insert(row?.to_lowercase());
282 }
283 Ok(columns)
284 }
285
286 fn lock(&self) -> Result<MutexGuard<'_, Connection>, MutationExecutorError> {
287 self.connection
288 .lock()
289 .map_err(|err| MutationExecutorError::Lock(err.to_string()))
290 }
291}
292
293impl teaql_data_service::DataServiceExecutor for SqliteMutationExecutor {
294 type Error = MutationExecutorError;
295
296 fn capabilities(&self) -> teaql_data_service::DataServiceCapabilities {
297 teaql_data_service::DataServiceCapabilities {
298 query: true,
299 mutation: true,
300 transaction: true,
301 schema: true,
302 id_generation: true,
303 ..Default::default()
304 }
305 }
306}
307
308impl SqlTransport for SqliteMutationExecutor {
309 type Error = MutationExecutorError;
310
311 async fn fetch_all_sql(&self, query: &CompiledQuery) -> Result<Vec<Record>, Self::Error> {
312 SqliteMutationExecutor::fetch_all(self, query)
313 }
314
315 async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
316 SqliteMutationExecutor::execute(self, query)
317 }
318}
319
320impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
321 async fn query_stream(
322 &self,
323 request: teaql_data_service::QueryRequest,
324 chunk_size: usize,
325 ) -> Result<Vec<teaql_data_service::StreamChunk>, Self::Error> {
326 let dialect = SqliteDialect;
327 let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
329 let compiled = dialect
330 .compile_select(&entity_desc, &request.query)
331 .map_err(MutationExecutorError::SqlCompile)?;
332 SqliteMutationExecutor::fetch_stream(self, &compiled, chunk_size)
333 }
334}
335
336impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
337 type Error = MutationExecutorError;
338
339 async fn commit_sql(self) -> Result<(), Self::Error> {
340 self.commit_transaction()
341 }
342
343 async fn rollback_sql(self) -> Result<(), Self::Error> {
344 self.rollback_transaction()
345 }
346}
347
348impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
349 type Tx<'a>
350 = Self
351 where
352 Self: 'a;
353
354 async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
355 self.begin_transaction()?;
356 Ok(self.clone())
357 }
358}
359
360fn initial_graph_exists_sqlite(
361 executor: &SqliteMutationExecutor,
362 dialect: &SqliteDialect,
363 entity: &EntityDescriptor,
364 graph: &GraphNode,
365) -> Result<bool, MutationExecutorError> {
366 let Some(id) = graph.values.get("id") else {
367 return Ok(false);
368 };
369 let query = dialect.compile_select(
370 entity,
371 &SelectQuery::new(&graph.entity)
372 .project("id")
373 .filter(Expr::eq("id", id.clone()))
374 .limit(1),
375 )?;
376 Ok(!executor.fetch_all(&query)?.is_empty())
377}
378
379fn compile_initial_graph_insert(
380 dialect: &impl SqlDialect,
381 entity: &EntityDescriptor,
382 graph: &GraphNode,
383) -> Result<CompiledQuery, MutationExecutorError> {
384 let mut command = InsertCommand::new(&graph.entity);
385 for (field, value) in &graph.values {
386 command = command.value(field.clone(), value.clone());
387 }
388 dialect.compile_insert(entity, &command).map_err(Into::into)
389}
390
391fn compile_initial_graph_update(
392 dialect: &impl SqlDialect,
393 entity: &EntityDescriptor,
394 graph: &GraphNode,
395) -> Result<Option<CompiledQuery>, MutationExecutorError> {
396 let Some(id) = graph.values.get("id") else {
397 return Ok(None);
398 };
399 let mut command = UpdateCommand::new(&graph.entity, id.clone());
400 for (field, value) in &graph.values {
401 if field == "id" {
402 continue;
403 }
404 command = command.value(field.clone(), value.clone());
405 }
406 match dialect.compile_update(entity, &command) {
407 Ok(query) => Ok(Some(query)),
408 Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
409 Err(err) => Err(err.into()),
410 }
411}
412
413pub trait SqliteSchemaExt {
414 fn ensure_sqlite_schema(
415 &self,
416 ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>>;
417}
418
419pub fn ensure_sqlite_schema_for(ctx: &UserContext) -> Result<(), MutationExecutorError> {
420 let dialect = ctx.get_resource::<SqliteDialect>().ok_or_else(|| {
421 MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
422 })?;
423 let executor = ctx
424 .get_resource::<SqliteMutationExecutor>()
425 .ok_or_else(|| {
426 MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
427 })?;
428
429 let entities = ctx.all_entities();
430
431 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
433
434 for entity in &entities {
436 let field_count = entity.properties.len();
437 if !executor.table_exists(&entity.table_name)? {
438 let sql = dialect.compile_create_table(entity)?;
440 executor.lock()?.execute(&sql, [])?;
441 let _ = ctx.send_event(RawAuditEvent::schema_created(
442 &entity.name,
443 &entity.table_name,
444 field_count,
445 ));
446 continue;
447 }
448 let existing_columns = executor.table_columns(&entity.table_name)?;
450 let mut fields_added = 0;
451 for property in &entity.properties {
452 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
453 if existing_columns.contains(&bare_column) {
454 continue;
455 }
456 let sql = dialect.compile_add_column(entity, property)?;
457 executor.lock()?.execute(&sql, [])?;
458 let _ = ctx.send_event(RawAuditEvent::field_added(
459 &entity.name,
460 &entity.table_name,
461 &property.column_name,
462 ));
463 fields_added += 1;
464 }
465 let _ = ctx.send_event(RawAuditEvent::schema_verified(
466 &entity.name,
467 &entity.table_name,
468 field_count,
469 ));
470 let _ = fields_added; }
472
473 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); for graph in ctx.initial_graphs() {
476 let entity = ctx.entity(&graph.entity).ok_or_else(|| {
477 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
478 })?;
479 let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
480 if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
481 if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
482 executor.execute(&query)?;
483 }
484 counts.1 += 1; continue;
486 }
487 let query = compile_initial_graph_insert(dialect, entity, graph)?;
488 executor.execute(&query)?;
489 counts.0 += 1; }
491
492 for (entity_name, (inserted, updated)) in &seed_counts {
494 let entity = ctx.entity(entity_name).ok_or_else(|| {
495 MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
496 })?;
497 let _ = ctx.send_event(RawAuditEvent::data_seeded(
498 entity_name,
499 &entity.table_name,
500 *inserted,
501 *updated,
502 ));
503 }
504
505 Ok(())
506}
507
508impl SqliteSchemaExt for UserContext {
509 fn ensure_sqlite_schema(
510 &self,
511 ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>> {
512 Box::pin(async move { ensure_sqlite_schema_for(self) })
513 }
514}
515
516#[derive(Debug, Default, Clone, Copy)]
517pub struct SqliteSchemaProvider;
518
519impl SchemaProvider for SqliteSchemaProvider {
520 fn ensure_schema<'a>(
521 &'a self,
522 ctx: &'a UserContext,
523 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
524 Box::pin(async move {
525 ensure_sqlite_schema_for(ctx).map_err(|err| RuntimeError::Schema(err.to_string()))
526 })
527 }
528}
529
530pub trait SqliteProviderExt {
531 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
532}
533
534impl SqliteProviderExt for UserContext {
535 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
536 self.insert_resource(SqliteDialect);
537 self.insert_resource(executor);
538 self.set_schema_provider(SqliteSchemaProvider);
539 self
540 }
541}
542
543#[derive(Clone)]
544pub struct SqliteIdSpaceGenerator {
545 executor: SqliteMutationExecutor,
546 table_name: String,
547}
548
549impl SqliteIdSpaceGenerator {
550 pub fn new(connection: Connection) -> Self {
551 Self::from_executor(SqliteMutationExecutor::from_connection(connection))
552 }
553
554 pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
555 Self {
556 executor,
557 table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
558 }
559 }
560
561 pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
562 self.table_name = table_name.into();
563 self
564 }
565
566 pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
567 self.executor.ensure_id_space_table(&self.table_name)
568 }
569
570 pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
571 self.ensure_table()?;
572 let sql = format!(
573 "INSERT INTO {} (type_name, current_level) VALUES (?, 1) \
574 ON CONFLICT (type_name) DO UPDATE \
575 SET current_level = current_level + 1 \
576 RETURNING current_level",
577 quote_ident(&self.table_name)
578 );
579 let id: i64 = self
580 .executor
581 .lock()?
582 .query_row(&sql, [entity], |row| row.get(0))?;
583 u64::try_from(id).map_err(|_| {
584 MutationExecutorError::Bind(format!("generated id {id} cannot be represented as u64"))
585 })
586 }
587}
588
589impl InternalIdGenerator for SqliteIdSpaceGenerator {
590 fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
591 self.next_id(entity)
592 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
593 }
594}
595
596fn quote_ident(ident: &str) -> String {
597 quote_identifier_if_needed(ident, '"')
598}
599
600fn strip_identifier_quotes(ident: &str) -> &str {
608 let bytes = ident.as_bytes();
609 if bytes.len() >= 2 {
610 let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
611 if (first == b'"' && last == b'"')
612 || (first == b'`' && last == b'`')
613 || (first == b'[' && last == b']')
614 {
615 return &ident[1..ident.len() - 1];
616 }
617 }
618 ident
619}
620
621fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
622 values.iter().map(bind_sqlite_value).collect()
623}
624
625fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
626 match value {
627 Value::Null => Ok(SqliteValue::Null),
628 Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
629 Value::I64(v) => Ok(SqliteValue::Integer(*v)),
630 Value::U64(v) => i64::try_from(*v)
631 .map(SqliteValue::Integer)
632 .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
633 Value::F64(v) => Ok(SqliteValue::Real(*v)),
634 Value::Decimal(v) => Ok(SqliteValue::Text(format!("{SQLITE_DECIMAL_PREFIX}{v}"))),
635 Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
636 Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
637 Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
638 Value::Timestamp(v) => Ok(SqliteValue::Text(v.0.to_string())),
639 Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
640 Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
641 Value::TypedNull(_) => Ok(SqliteValue::Null),
642 }
643}
644
645#[derive(Debug, Clone)]
646struct ColumnInfo {
647 name: String,
648 decl_type: Option<String>,
649}
650
651fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
652 statement
653 .columns()
654 .into_iter()
655 .map(|column| ColumnInfo {
656 name: column.name().to_owned(),
657 decl_type: column.decl_type().map(|value| value.to_ascii_uppercase()),
658 })
659 .collect()
660}
661
662fn decode_sqlite_row(
663 row: &Row<'_>,
664 columns: &[ColumnInfo],
665) -> Result<Record, MutationExecutorError> {
666 let mut record = BTreeMap::new();
667 for (index, column) in columns.iter().enumerate() {
668 let value_ref = row.get_ref(index)?;
669 let value = match value_ref {
670 ValueRef::Null => Value::Null,
671 ValueRef::Integer(value) => decode_sqlite_integer(value, column),
672 ValueRef::Real(value) => Value::F64(value),
673 ValueRef::Text(value) => decode_sqlite_text(value, column)?,
674 ValueRef::Blob(_) => {
675 return Err(MutationExecutorError::UnsupportedColumnType(
676 "BLOB".to_owned(),
677 ));
678 }
679 };
680 record.insert(column.name.clone(), value);
681 }
682 Ok(record)
683}
684
685fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
686 match column_decl_type(column).as_deref() {
687 Some("BOOLEAN") | Some("BOOL") => Value::Bool(value != 0),
688 _ => Value::I64(value),
689 }
690}
691
692fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
693 let value = std::str::from_utf8(value)
694 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
695 if let Some(decimal) = value.strip_prefix(SQLITE_DECIMAL_PREFIX) {
696 return Decimal::from_str(decimal)
697 .map(Value::Decimal)
698 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}")));
699 }
700
701 match column_decl_type(column).as_deref() {
702 Some("NUMERIC") | Some("DECIMAL") => Decimal::from_str(value)
703 .map(Value::Decimal)
704 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
705 Some("JSON") => serde_json::from_str(value).map(Value::Json).map_err(|err| {
706 MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
707 }),
708 Some("DATE") => NaiveDate::parse_from_str(value, "%Y-%m-%d")
709 .map(Value::Date)
710 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
711 Some("TIMESTAMP") | Some("DATETIME") => parse_sqlite_timestamp(value),
712 _ => infer_sqlite_text(value),
713 }
714}
715
716fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
717 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
718 return Ok(Value::Date(date));
719 }
720 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
721 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
722 timestamp.timestamp_millis(),
723 )));
724 }
725 if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
726 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
727 timestamp.and_utc().timestamp_millis(),
728 )));
729 }
730 Ok(Value::Text(value.to_owned()))
731}
732
733fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
734 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
735 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
736 timestamp.timestamp_millis(),
737 )));
738 }
739 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
740 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
741 date.and_hms_opt(0, 0, 0)
742 .unwrap_or_default()
743 .and_utc()
744 .timestamp_millis(),
745 )));
746 }
747 NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
748 .map(|timestamp| {
749 Value::Timestamp(teaql_core::time::Timestamp(
750 timestamp.and_utc().timestamp_millis(),
751 ))
752 })
753 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
754}
755
756fn column_decl_type(column: &ColumnInfo) -> Option<String> {
757 column
758 .decl_type
759 .as_ref()
760 .map(|value| value.split('(').next().unwrap_or(value).trim().to_owned())
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use teaql_core::{DeleteCommand, RecoverCommand};
767 use teaql_macros::TeaqlEntity;
768 use teaql_runtime::InMemoryMetadataStore;
769
770 fn entity() -> EntityDescriptor {
771 EntityDescriptor::new("Order")
772 .table_name("orders")
773 .property(
774 PropertyDescriptor::new("id", DataType::U64)
775 .column_name("id")
776 .id()
777 .not_null(),
778 )
779 .property(
780 PropertyDescriptor::new("version", DataType::I64)
781 .column_name("version")
782 .version()
783 .not_null(),
784 )
785 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
786 }
787
788 #[allow(dead_code)]
789 #[derive(Debug, PartialEq, TeaqlEntity)]
790 #[teaql(entity = "FeatureFlag", table = "feature_flags")]
791 struct FeatureFlagRow {
792 #[teaql(id)]
793 id: u64,
794 #[teaql(version)]
795 version: i64,
796 enabled: bool,
797 optional_enabled: Option<bool>,
798 }
799
800 fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
801 Record::from([
802 ("id".to_owned(), Value::U64(1)),
803 ("version".to_owned(), Value::I64(1)),
804 ("enabled".to_owned(), enabled),
805 ("optional_enabled".to_owned(), optional_enabled),
806 ])
807 }
808
809 #[test]
810 fn sqlite_dialect_compiles_mutations_and_schema() {
811 let insert = SqliteDialect
812 .compile_insert(
813 &entity(),
814 &InsertCommand::new("Order")
815 .value("id", 1_u64)
816 .value("name", "A"),
817 )
818 .unwrap();
819 assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
820
821 let update = SqliteDialect
822 .compile_update(
823 &entity(),
824 &UpdateCommand::new("Order", 1_u64)
825 .expected_version(3)
826 .value("name", "B"),
827 )
828 .unwrap();
829 assert_eq!(
830 update.sql,
831 "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
832 );
833
834 let delete = SqliteDialect
835 .compile_delete(
836 &entity(),
837 &DeleteCommand::new("Order", 1_u64).expected_version(3),
838 )
839 .unwrap();
840 let recover = SqliteDialect
841 .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
842 .unwrap();
843 assert_eq!(
844 delete.sql,
845 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
846 );
847 assert_eq!(
848 recover.sql,
849 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
850 );
851
852 let create = SqliteDialect.compile_create_table(&entity()).unwrap();
853 assert_eq!(
854 create,
855 "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
856 );
857 }
858
859 #[test]
860 fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
861 let executor =
862 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
863 let entity = entity();
864 let mut ctx = UserContext::new()
865 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
866
867 ctx.use_sqlite_provider(executor.clone());
868 ensure_sqlite_schema_for(&ctx).unwrap();
869
870 let insert = SqliteDialect
871 .compile_insert(
872 &entity,
873 &InsertCommand::new("Order")
874 .value("id", 1_u64)
875 .value("version", 1_i64)
876 .value("name", "draft"),
877 )
878 .unwrap();
879 assert_eq!(executor.execute(&insert).unwrap(), 1);
880
881 let select = SqliteDialect
882 .compile_select(
883 &entity,
884 &SelectQuery::new("Order")
885 .filter(Expr::eq("id", 1_u64))
886 .order_asc("id"),
887 )
888 .unwrap();
889 let rows = executor.fetch_all(&select).unwrap();
890 assert_eq!(rows.len(), 1);
891 assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
892 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
893 assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
894 }
895
896 #[test]
897 fn sqlite_boolean_new_schema_roundtrips_as_bool() {
898 let executor =
899 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
900 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
901 let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
902 assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
903 assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
904 assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
905
906 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
907 for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
908 let insert = SqliteDialect
909 .compile_insert(
910 &entity,
911 &InsertCommand::new("FeatureFlag")
912 .value("id", id)
913 .value("version", 1_i64)
914 .value("enabled", enabled)
915 .value("optional_enabled", optional_enabled),
916 )
917 .unwrap();
918 assert_eq!(executor.execute(&insert).unwrap(), 1);
919 }
920
921 let select = SqliteDialect
922 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
923 .unwrap();
924 let rows = executor.fetch_all(&select).unwrap();
925 assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
926 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
927 assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
928 assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
929
930 let first = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[0].clone()).unwrap();
931 let second = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[1].clone()).unwrap();
932 assert!(!first.enabled);
933 assert_eq!(first.optional_enabled, Some(true));
934 assert!(second.enabled);
935 assert_eq!(second.optional_enabled, Some(false));
936 }
937
938 #[test]
939 fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
940 let executor =
941 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
942 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
943 executor
944 .execute(&CompiledQuery {
945 sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
946 .to_owned(),
947 params: Vec::new(),
948 comment: None,
949 })
950 .unwrap();
951
952 let insert = SqliteDialect
953 .compile_insert(
954 &entity,
955 &InsertCommand::new("FeatureFlag")
956 .value("id", 1_u64)
957 .value("version", 1_i64)
958 .value("enabled", true)
959 .value("optional_enabled", false),
960 )
961 .unwrap();
962 executor.execute(&insert).unwrap();
963 executor
964 .execute(&CompiledQuery {
965 sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
966 .to_owned(),
967 params: vec![
968 Value::U64(2),
969 Value::I64(1),
970 Value::I64(2),
971 Value::Null,
972 ],
973 comment: None,
974 })
975 .unwrap();
976 let select = SqliteDialect
977 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
978 .unwrap();
979 let rows = executor.fetch_all(&select).unwrap();
980 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
981 assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
982 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
983
984 let decoded = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[0].clone()).unwrap();
985 assert!(decoded.enabled);
986 assert_eq!(decoded.optional_enabled, Some(false));
987 assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
988 let error =
989 <FeatureFlagRow as teaql_core::Entity>::from_record(rows[1].clone()).unwrap_err();
990 assert!(error.message.contains("invalid field enabled"));
991
992 for (value, expected) in [
993 (Value::I64(0), false),
994 (Value::I64(1), true),
995 (Value::U64(0), false),
996 (Value::U64(1), true),
997 ] {
998 let decoded = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
999 value,
1000 Value::Null,
1001 ))
1002 .unwrap();
1003 assert_eq!(decoded.enabled, expected);
1004 assert_eq!(decoded.optional_enabled, None);
1005 }
1006
1007 for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
1008 let error = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1009 invalid,
1010 Value::Null,
1011 ))
1012 .unwrap_err();
1013 assert!(error.message.contains("invalid field enabled"));
1014 }
1015 let error = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1016 Value::Bool(true),
1017 Value::U64(2),
1018 ))
1019 .unwrap_err();
1020 assert!(error.message.contains("invalid field optional_enabled"));
1021 }
1022
1023 #[test]
1024 fn sqlite_executor_parses_json_only_for_json_columns() {
1025 let executor =
1026 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1027
1028 executor
1029 .execute(&CompiledQuery {
1030 sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
1031 params: Vec::new(),
1032 comment: None,
1033 })
1034 .unwrap();
1035 executor
1036 .execute(&CompiledQuery {
1037 sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
1038 params: vec![
1039 Value::Text("{\"active\":true}".to_owned()),
1040 Value::Json(serde_json::json!({"active": true})),
1041 ],
1042 comment: None,
1043 })
1044 .unwrap();
1045
1046 let rows = executor
1047 .fetch_all(&CompiledQuery {
1048 sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
1049 params: Vec::new(),
1050 comment: None,
1051 })
1052 .unwrap();
1053
1054 assert_eq!(
1055 rows[0].get("text_payload"),
1056 Some(&Value::Text("{\"active\":true}".to_owned()))
1057 );
1058 assert_eq!(
1059 rows[0].get("json_payload"),
1060 Some(&Value::Json(serde_json::json!({"active": true})))
1061 );
1062 }
1063
1064 #[test]
1065 fn sqlite_id_space_generator_increments_ids() {
1066 let executor =
1067 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1068 let generator = SqliteIdSpaceGenerator::from_executor(executor);
1069 assert_eq!(generator.next_id("Order").unwrap(), 1);
1070 assert_eq!(generator.next_id("Order").unwrap(), 2);
1071 }
1072
1073 #[test]
1074 fn sqlite_fetch_stream_returns_chunked_rows() {
1075 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1076 Connection::open_in_memory().unwrap(),
1077 )));
1078 let entity = entity();
1079
1080 executor
1082 .execute(&CompiledQuery {
1083 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1084 .to_owned(),
1085 params: Vec::new(),
1086 comment: None,
1087 })
1088 .unwrap();
1089
1090 for i in 1..=25 {
1091 let insert = SqliteDialect
1092 .compile_insert(
1093 &entity,
1094 &InsertCommand::new("Order")
1095 .value("id", i as u64)
1096 .value("version", 1_i64)
1097 .value("name", format!("order-{i}")),
1098 )
1099 .unwrap();
1100 executor.execute(&insert).unwrap();
1101 }
1102
1103 let query = SelectQuery::new("Order")
1105 .filter(Expr::gt("version", 0_i64))
1106 .order_asc("id")
1107 .stream(10);
1108
1109 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1110
1111 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1112
1113 assert_eq!(chunks.len(), 3);
1115 assert_eq!(chunks[0].rows.len(), 10);
1116 assert_eq!(chunks[0].chunk_index, 0);
1117 assert!(!chunks[0].is_last);
1118
1119 assert_eq!(chunks[1].rows.len(), 10);
1120 assert_eq!(chunks[1].chunk_index, 1);
1121 assert!(!chunks[1].is_last);
1122
1123 assert_eq!(chunks[2].rows.len(), 5);
1124 assert_eq!(chunks[2].chunk_index, 2);
1125 assert!(chunks[2].is_last);
1126
1127 assert_eq!(
1129 chunks[0].rows[0].get("name"),
1130 Some(&Value::Text("order-1".to_owned()))
1131 );
1132 assert_eq!(
1133 chunks[2].rows[4].get("name"),
1134 Some(&Value::Text("order-25".to_owned()))
1135 );
1136 }
1137
1138 #[test]
1139 fn sqlite_fetch_stream_handles_empty_result() {
1140 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1141 Connection::open_in_memory().unwrap(),
1142 )));
1143
1144 executor
1145 .execute(&CompiledQuery {
1146 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1147 .to_owned(),
1148 params: Vec::new(),
1149 comment: None,
1150 })
1151 .unwrap();
1152
1153 let entity = entity();
1154 let query = SelectQuery::new("Order")
1155 .filter(Expr::gt("version", 0_i64))
1156 .stream(10);
1157
1158 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1159
1160 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1161
1162 assert_eq!(chunks.len(), 1);
1164 assert_eq!(chunks[0].rows.len(), 0);
1165 assert!(chunks[0].is_last);
1166 }
1167
1168 #[test]
1169 fn sqlite_fetch_stream_exact_chunk_boundary() {
1170 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1171 Connection::open_in_memory().unwrap(),
1172 )));
1173 let entity = entity();
1174
1175 executor
1176 .execute(&CompiledQuery {
1177 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1178 .to_owned(),
1179 params: Vec::new(),
1180 comment: None,
1181 })
1182 .unwrap();
1183
1184 for i in 1..=20 {
1186 let insert = SqliteDialect
1187 .compile_insert(
1188 &entity,
1189 &InsertCommand::new("Order")
1190 .value("id", i as u64)
1191 .value("version", 1_i64)
1192 .value("name", format!("order-{i}")),
1193 )
1194 .unwrap();
1195 executor.execute(&insert).unwrap();
1196 }
1197
1198 let query = SelectQuery::new("Order")
1199 .filter(Expr::gt("version", 0_i64))
1200 .order_asc("id")
1201 .stream(10);
1202
1203 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1204
1205 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1206
1207 assert_eq!(chunks.len(), 3);
1209 assert_eq!(chunks[0].rows.len(), 10);
1210 assert!(!chunks[0].is_last);
1211 assert_eq!(chunks[1].rows.len(), 10);
1212 assert!(!chunks[1].is_last);
1213 assert_eq!(chunks[2].rows.len(), 0);
1214 assert!(chunks[2].is_last);
1215 }
1216
1217 #[test]
1218 fn test_parse_sqlite_timestamp() {
1219 let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
1220 assert!(matches!(ts1, Value::Timestamp(_)));
1221
1222 let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
1223 assert!(matches!(ts2, Value::Timestamp(_)));
1224
1225 let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
1226 assert!(matches!(ts3, Value::Timestamp(_)));
1227
1228 assert!(parse_sqlite_timestamp("invalid").is_err());
1229 }
1230}