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, TimeZone, Utc};
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("INTEGER"),
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("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 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(&self, query: &CompiledQuery) -> Result<Vec<Record>, MutationExecutorError> {
209 let params = bind_values(&query.params)?;
210 let connection = self.lock()?;
211 let mut statement = connection.prepare(&query.sql_with_comment())?;
212 let columns = statement_columns(&statement);
213 let mut rows = statement.query(params_from_iter(params.iter()))?;
214 let mut records = Vec::new();
215 while let Some(row) = rows.next()? {
216 records.push(decode_sqlite_row(row, &columns)?);
217 }
218 Ok(records)
219 }
220
221 pub fn fetch_stream(
224 &self,
225 query: &CompiledQuery,
226 chunk_size: usize,
227 ) -> Result<Vec<teaql_data_service::StreamChunk>, MutationExecutorError> {
228 let params = bind_values(&query.params)?;
229 let connection = self.lock()?;
230 let mut statement = connection.prepare(&query.sql_with_comment())?;
231 let columns = statement_columns(&statement);
232 let mut rows = statement.query(params_from_iter(params.iter()))?;
233
234 let mut chunks = Vec::new();
235 let mut current_chunk = Vec::new();
236 let mut chunk_index = 0;
237
238 while let Some(row) = rows.next()? {
239 current_chunk.push(decode_sqlite_row(row, &columns)?);
240 if current_chunk.len() >= chunk_size {
241 chunks.push(teaql_data_service::StreamChunk {
242 rows: current_chunk,
243 chunk_index,
244 is_last: false,
245 });
246 current_chunk = Vec::new();
247 chunk_index += 1;
248 }
249 }
250
251 chunks.push(teaql_data_service::StreamChunk {
253 rows: current_chunk,
254 chunk_index,
255 is_last: true,
256 });
257
258 Ok(chunks)
259 }
260
261 pub fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
262 let exists: i64 = self.lock()?.query_row(
263 "SELECT COUNT(1) FROM sqlite_master WHERE type = 'table' AND name = ?",
264 [table_name],
265 |row| row.get(0),
266 )?;
267 Ok(exists > 0)
268 }
269
270 pub fn table_columns(
271 &self,
272 table_name: &str,
273 ) -> Result<BTreeSet<String>, MutationExecutorError> {
274 let pragma_sql = format!("PRAGMA table_info({})", quote_ident(table_name));
275 let connection = self.lock()?;
276 let mut statement = connection.prepare(&pragma_sql)?;
277 let rows = statement.query_map([], |row| row.get::<_, String>("name"))?;
278 let mut columns = BTreeSet::new();
279 for row in rows {
280 columns.insert(row?.to_lowercase());
281 }
282 Ok(columns)
283 }
284
285 fn lock(&self) -> Result<MutexGuard<'_, Connection>, MutationExecutorError> {
286 self.connection
287 .lock()
288 .map_err(|err| MutationExecutorError::Lock(err.to_string()))
289 }
290}
291
292impl teaql_data_service::DataServiceExecutor for SqliteMutationExecutor {
293 type Error = MutationExecutorError;
294
295 fn capabilities(&self) -> teaql_data_service::DataServiceCapabilities {
296 teaql_data_service::DataServiceCapabilities {
297 query: true,
298 mutation: true,
299 transaction: true,
300 schema: true,
301 id_generation: true,
302 ..Default::default()
303 }
304 }
305}
306
307impl SqlTransport for SqliteMutationExecutor {
308 type Error = MutationExecutorError;
309
310 async fn fetch_all_sql(&self, query: &CompiledQuery) -> Result<Vec<Record>, Self::Error> {
311 SqliteMutationExecutor::fetch_all(self, query)
312 }
313
314 async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
315 SqliteMutationExecutor::execute(self, query)
316 }
317}
318
319impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
320 async fn query_stream(
321 &self,
322 request: teaql_data_service::QueryRequest,
323 chunk_size: usize,
324 ) -> Result<Vec<teaql_data_service::StreamChunk>, Self::Error> {
325 let dialect = SqliteDialect;
326 let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
328 let compiled = dialect
329 .compile_select(&entity_desc, &request.query)
330 .map_err(MutationExecutorError::SqlCompile)?;
331 SqliteMutationExecutor::fetch_stream(self, &compiled, chunk_size)
332 }
333}
334
335impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
336 type Error = MutationExecutorError;
337
338 async fn commit_sql(self) -> Result<(), Self::Error> {
339 self.commit_transaction()
340 }
341
342 async fn rollback_sql(self) -> Result<(), Self::Error> {
343 self.rollback_transaction()
344 }
345}
346
347impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
348 type Tx<'a>
349 = Self
350 where
351 Self: 'a;
352
353 async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
354 self.begin_transaction()?;
355 Ok(self.clone())
356 }
357}
358
359fn initial_graph_exists_sqlite(
360 executor: &SqliteMutationExecutor,
361 dialect: &SqliteDialect,
362 entity: &EntityDescriptor,
363 graph: &GraphNode,
364) -> Result<bool, MutationExecutorError> {
365 let Some(id) = graph.values.get("id") else {
366 return Ok(false);
367 };
368 let query = dialect.compile_select(
369 entity,
370 &SelectQuery::new(&graph.entity)
371 .project("id")
372 .filter(Expr::eq("id", id.clone()))
373 .limit(1),
374 )?;
375 Ok(!executor.fetch_all(&query)?.is_empty())
376}
377
378fn compile_initial_graph_insert(
379 dialect: &impl SqlDialect,
380 entity: &EntityDescriptor,
381 graph: &GraphNode,
382) -> Result<CompiledQuery, MutationExecutorError> {
383 let mut command = InsertCommand::new(&graph.entity);
384 for (field, value) in &graph.values {
385 command = command.value(field.clone(), value.clone());
386 }
387 dialect.compile_insert(entity, &command).map_err(Into::into)
388}
389
390fn compile_initial_graph_update(
391 dialect: &impl SqlDialect,
392 entity: &EntityDescriptor,
393 graph: &GraphNode,
394) -> Result<Option<CompiledQuery>, MutationExecutorError> {
395 let Some(id) = graph.values.get("id") else {
396 return Ok(None);
397 };
398 let mut command = UpdateCommand::new(&graph.entity, id.clone());
399 for (field, value) in &graph.values {
400 if field == "id" {
401 continue;
402 }
403 command = command.value(field.clone(), value.clone());
404 }
405 match dialect.compile_update(entity, &command) {
406 Ok(query) => Ok(Some(query)),
407 Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
408 Err(err) => Err(err.into()),
409 }
410}
411
412pub trait SqliteSchemaExt {
413 fn ensure_sqlite_schema(
414 &self,
415 ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>>;
416}
417
418pub fn ensure_sqlite_schema_for(ctx: &UserContext) -> Result<(), MutationExecutorError> {
419 let dialect = ctx.get_resource::<SqliteDialect>().ok_or_else(|| {
420 MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
421 })?;
422 let executor = ctx
423 .get_resource::<SqliteMutationExecutor>()
424 .ok_or_else(|| {
425 MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
426 })?;
427
428 let entities = ctx.all_entities();
429
430 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
432
433 for entity in &entities {
435 let field_count = entity.properties.len();
436 if !executor.table_exists(&entity.table_name)? {
437 let sql = dialect.compile_create_table(entity)?;
439 executor.lock()?.execute(&sql, [])?;
440 let _ = ctx.send_event(RawAuditEvent::schema_created(
441 &entity.name,
442 &entity.table_name,
443 field_count,
444 ));
445 continue;
446 }
447 let existing_columns = executor.table_columns(&entity.table_name)?;
449 let mut fields_added = 0;
450 for property in &entity.properties {
451 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
452 if existing_columns.contains(&bare_column) {
453 continue;
454 }
455 let sql = dialect.compile_add_column(entity, property)?;
456 executor.lock()?.execute(&sql, [])?;
457 let _ = ctx.send_event(RawAuditEvent::field_added(
458 &entity.name,
459 &entity.table_name,
460 &property.column_name,
461 ));
462 fields_added += 1;
463 }
464 let _ = ctx.send_event(RawAuditEvent::schema_verified(
465 &entity.name,
466 &entity.table_name,
467 field_count,
468 ));
469 let _ = fields_added; }
471
472 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); for graph in ctx.initial_graphs() {
475 let entity = ctx.entity(&graph.entity).ok_or_else(|| {
476 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
477 })?;
478 let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
479 if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
480 if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
481 executor.execute(&query)?;
482 }
483 counts.1 += 1; continue;
485 }
486 let query = compile_initial_graph_insert(dialect, entity, graph)?;
487 executor.execute(&query)?;
488 counts.0 += 1; }
490
491 for (entity_name, (inserted, updated)) in &seed_counts {
493 let entity = ctx.entity(entity_name).ok_or_else(|| {
494 MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
495 })?;
496 let _ = ctx.send_event(RawAuditEvent::data_seeded(
497 entity_name,
498 &entity.table_name,
499 *inserted,
500 *updated,
501 ));
502 }
503
504 Ok(())
505}
506
507impl SqliteSchemaExt for UserContext {
508 fn ensure_sqlite_schema(
509 &self,
510 ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>> {
511 Box::pin(async move { ensure_sqlite_schema_for(self) })
512 }
513}
514
515#[derive(Debug, Default, Clone, Copy)]
516pub struct SqliteSchemaProvider;
517
518impl SchemaProvider for SqliteSchemaProvider {
519 fn ensure_schema<'a>(
520 &'a self,
521 ctx: &'a UserContext,
522 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
523 Box::pin(async move {
524 ensure_sqlite_schema_for(ctx).map_err(|err| RuntimeError::Schema(err.to_string()))
525 })
526 }
527}
528
529pub trait SqliteProviderExt {
530 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
531}
532
533impl SqliteProviderExt for UserContext {
534 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
535 self.insert_resource(SqliteDialect);
536 self.insert_resource(executor);
537 self.set_schema_provider(SqliteSchemaProvider);
538 self
539 }
540}
541
542#[derive(Clone)]
543pub struct SqliteIdSpaceGenerator {
544 executor: SqliteMutationExecutor,
545 table_name: String,
546}
547
548impl SqliteIdSpaceGenerator {
549 pub fn new(connection: Connection) -> Self {
550 Self::from_executor(SqliteMutationExecutor::from_connection(connection))
551 }
552
553 pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
554 Self {
555 executor,
556 table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
557 }
558 }
559
560 pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
561 self.table_name = table_name.into();
562 self
563 }
564
565 pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
566 self.executor.ensure_id_space_table(&self.table_name)
567 }
568
569 pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
570 self.ensure_table()?;
571 let sql = format!(
572 "INSERT INTO {} (type_name, current_level) VALUES (?, 1) \
573 ON CONFLICT (type_name) DO UPDATE \
574 SET current_level = current_level + 1 \
575 RETURNING current_level",
576 quote_ident(&self.table_name)
577 );
578 let id: i64 = self
579 .executor
580 .lock()?
581 .query_row(&sql, [entity], |row| row.get(0))?;
582 u64::try_from(id).map_err(|_| {
583 MutationExecutorError::Bind(format!("generated id {id} cannot be represented as u64"))
584 })
585 }
586}
587
588impl InternalIdGenerator for SqliteIdSpaceGenerator {
589 fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
590 self.next_id(entity)
591 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
592 }
593}
594
595fn quote_ident(ident: &str) -> String {
596 quote_identifier_if_needed(ident, '"')
597}
598
599fn strip_identifier_quotes(ident: &str) -> &str {
607 let bytes = ident.as_bytes();
608 if bytes.len() >= 2 {
609 let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
610 if (first == b'"' && last == b'"')
611 || (first == b'`' && last == b'`')
612 || (first == b'[' && last == b']')
613 {
614 return &ident[1..ident.len() - 1];
615 }
616 }
617 ident
618}
619
620fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
621 values.iter().map(bind_sqlite_value).collect()
622}
623
624fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
625 match value {
626 Value::Null => Ok(SqliteValue::Null),
627 Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
628 Value::I64(v) => Ok(SqliteValue::Integer(*v)),
629 Value::U64(v) => i64::try_from(*v)
630 .map(SqliteValue::Integer)
631 .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
632 Value::F64(v) => Ok(SqliteValue::Real(*v)),
633 Value::Decimal(v) => Ok(SqliteValue::Text(format!("{SQLITE_DECIMAL_PREFIX}{v}"))),
634 Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
635 Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
636 Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
637 Value::Timestamp(v) => Ok(SqliteValue::Text(v.to_rfc3339())),
638 Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
639 Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
640 }
641}
642
643#[derive(Debug, Clone)]
644struct ColumnInfo {
645 name: String,
646 decl_type: Option<String>,
647}
648
649fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
650 statement
651 .columns()
652 .into_iter()
653 .map(|column| ColumnInfo {
654 name: column.name().to_owned(),
655 decl_type: column.decl_type().map(|value| value.to_ascii_uppercase()),
656 })
657 .collect()
658}
659
660fn decode_sqlite_row(
661 row: &Row<'_>,
662 columns: &[ColumnInfo],
663) -> Result<Record, MutationExecutorError> {
664 let mut record = BTreeMap::new();
665 for (index, column) in columns.iter().enumerate() {
666 let value_ref = row.get_ref(index)?;
667 let value = match value_ref {
668 ValueRef::Null => Value::Null,
669 ValueRef::Integer(value) => decode_sqlite_integer(value, column),
670 ValueRef::Real(value) => Value::F64(value),
671 ValueRef::Text(value) => decode_sqlite_text(value, column)?,
672 ValueRef::Blob(_) => {
673 return Err(MutationExecutorError::UnsupportedColumnType(
674 "BLOB".to_owned(),
675 ));
676 }
677 };
678 record.insert(column.name.clone(), value);
679 }
680 Ok(record)
681}
682
683fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
684 match column_decl_type(column).as_deref() {
685 Some("BOOLEAN") | Some("BOOL") => Value::Bool(value != 0),
686 _ => Value::I64(value),
687 }
688}
689
690fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
691 let value = std::str::from_utf8(value)
692 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
693 if let Some(decimal) = value.strip_prefix(SQLITE_DECIMAL_PREFIX) {
694 return Decimal::from_str(decimal)
695 .map(Value::Decimal)
696 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}")));
697 }
698
699 match column_decl_type(column).as_deref() {
700 Some("NUMERIC") | Some("DECIMAL") => Decimal::from_str(value)
701 .map(Value::Decimal)
702 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
703 Some("JSON") => serde_json::from_str(value).map(Value::Json).map_err(|err| {
704 MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
705 }),
706 Some("DATE") => NaiveDate::parse_from_str(value, "%Y-%m-%d")
707 .map(Value::Date)
708 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
709 Some("TIMESTAMP") | Some("DATETIME") => parse_sqlite_timestamp(value),
710 _ => infer_sqlite_text(value),
711 }
712}
713
714fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
715 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
716 return Ok(Value::Date(date));
717 }
718 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
719 return Ok(Value::Timestamp(timestamp.with_timezone(&Utc)));
720 }
721 if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
722 return Ok(Value::Timestamp(Utc.from_utc_datetime(×tamp)));
723 }
724 Ok(Value::Text(value.to_owned()))
725}
726
727fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
728 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
729 return Ok(Value::Timestamp(timestamp.with_timezone(&Utc)));
730 }
731 NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
732 .map(|timestamp| Value::Timestamp(Utc.from_utc_datetime(×tamp)))
733 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
734}
735
736fn column_decl_type(column: &ColumnInfo) -> Option<String> {
737 column
738 .decl_type
739 .as_ref()
740 .map(|value| value.split('(').next().unwrap_or(value).trim().to_owned())
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use teaql_core::{DeleteCommand, RecoverCommand};
747 use teaql_runtime::InMemoryMetadataStore;
748
749 fn entity() -> EntityDescriptor {
750 EntityDescriptor::new("Order")
751 .table_name("orders")
752 .property(
753 PropertyDescriptor::new("id", DataType::U64)
754 .column_name("id")
755 .id()
756 .not_null(),
757 )
758 .property(
759 PropertyDescriptor::new("version", DataType::I64)
760 .column_name("version")
761 .version()
762 .not_null(),
763 )
764 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
765 }
766
767 #[test]
768 fn sqlite_dialect_compiles_mutations_and_schema() {
769 let insert = SqliteDialect
770 .compile_insert(
771 &entity(),
772 &InsertCommand::new("Order")
773 .value("id", 1_u64)
774 .value("name", "A"),
775 )
776 .unwrap();
777 assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
778
779 let update = SqliteDialect
780 .compile_update(
781 &entity(),
782 &UpdateCommand::new("Order", 1_u64)
783 .expected_version(3)
784 .value("name", "B"),
785 )
786 .unwrap();
787 assert_eq!(
788 update.sql,
789 "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
790 );
791
792 let delete = SqliteDialect
793 .compile_delete(
794 &entity(),
795 &DeleteCommand::new("Order", 1_u64).expected_version(3),
796 )
797 .unwrap();
798 let recover = SqliteDialect
799 .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
800 .unwrap();
801 assert_eq!(
802 delete.sql,
803 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
804 );
805 assert_eq!(
806 recover.sql,
807 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
808 );
809
810 let create = SqliteDialect.compile_create_table(&entity()).unwrap();
811 assert_eq!(
812 create,
813 "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name TEXT)"
814 );
815 }
816
817 #[test]
818 fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
819 let executor =
820 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
821 let entity = entity();
822 let mut ctx = UserContext::new()
823 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
824
825 ctx.use_sqlite_provider(executor.clone());
826 ensure_sqlite_schema_for(&ctx).unwrap();
827
828 let insert = SqliteDialect
829 .compile_insert(
830 &entity,
831 &InsertCommand::new("Order")
832 .value("id", 1_u64)
833 .value("version", 1_i64)
834 .value("name", "draft"),
835 )
836 .unwrap();
837 assert_eq!(executor.execute(&insert).unwrap(), 1);
838
839 let select = SqliteDialect
840 .compile_select(
841 &entity,
842 &SelectQuery::new("Order")
843 .filter(Expr::eq("id", 1_u64))
844 .order_asc("id"),
845 )
846 .unwrap();
847 let rows = executor.fetch_all(&select).unwrap();
848 assert_eq!(rows.len(), 1);
849 assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
850 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
851 assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
852 }
853
854 #[test]
855 fn sqlite_executor_parses_json_only_for_json_columns() {
856 let executor =
857 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
858
859 executor
860 .execute(&CompiledQuery {
861 sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
862 params: Vec::new(),
863 comment: None,
864 })
865 .unwrap();
866 executor
867 .execute(&CompiledQuery {
868 sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
869 params: vec![
870 Value::Text("{\"active\":true}".to_owned()),
871 Value::Json(serde_json::json!({"active": true})),
872 ],
873 comment: None,
874 })
875 .unwrap();
876
877 let rows = executor
878 .fetch_all(&CompiledQuery {
879 sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
880 params: Vec::new(),
881 comment: None,
882 })
883 .unwrap();
884
885 assert_eq!(
886 rows[0].get("text_payload"),
887 Some(&Value::Text("{\"active\":true}".to_owned()))
888 );
889 assert_eq!(
890 rows[0].get("json_payload"),
891 Some(&Value::Json(serde_json::json!({"active": true})))
892 );
893 }
894
895 #[test]
896 fn sqlite_id_space_generator_increments_ids() {
897 let executor =
898 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
899 let generator = SqliteIdSpaceGenerator::from_executor(executor);
900 assert_eq!(generator.next_id("Order").unwrap(), 1);
901 assert_eq!(generator.next_id("Order").unwrap(), 2);
902 }
903
904 #[test]
905 fn sqlite_fetch_stream_returns_chunked_rows() {
906 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(Connection::open_in_memory().unwrap())));
907 let entity = entity();
908
909 executor
911 .execute(&CompiledQuery {
912 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT)"
913 .to_owned(),
914 params: Vec::new(),
915 comment: None,
916 })
917 .unwrap();
918
919 for i in 1..=25 {
920 let insert = SqliteDialect
921 .compile_insert(
922 &entity,
923 &InsertCommand::new("Order")
924 .value("id", i as u64)
925 .value("version", 1_i64)
926 .value("name", format!("order-{i}")),
927 )
928 .unwrap();
929 executor.execute(&insert).unwrap();
930 }
931
932 let query = SelectQuery::new("Order")
934 .filter(Expr::gt("version", 0_i64))
935 .order_asc("id")
936 .stream(10);
937
938 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
939
940 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
941
942 assert_eq!(chunks.len(), 3);
944 assert_eq!(chunks[0].rows.len(), 10);
945 assert_eq!(chunks[0].chunk_index, 0);
946 assert!(!chunks[0].is_last);
947
948 assert_eq!(chunks[1].rows.len(), 10);
949 assert_eq!(chunks[1].chunk_index, 1);
950 assert!(!chunks[1].is_last);
951
952 assert_eq!(chunks[2].rows.len(), 5);
953 assert_eq!(chunks[2].chunk_index, 2);
954 assert!(chunks[2].is_last);
955
956 assert_eq!(
958 chunks[0].rows[0].get("name"),
959 Some(&Value::Text("order-1".to_owned()))
960 );
961 assert_eq!(
962 chunks[2].rows[4].get("name"),
963 Some(&Value::Text("order-25".to_owned()))
964 );
965 }
966
967 #[test]
968 fn sqlite_fetch_stream_handles_empty_result() {
969 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(Connection::open_in_memory().unwrap())));
970
971 executor
972 .execute(&CompiledQuery {
973 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT)"
974 .to_owned(),
975 params: Vec::new(),
976 comment: None,
977 })
978 .unwrap();
979
980 let entity = entity();
981 let query = SelectQuery::new("Order")
982 .filter(Expr::gt("version", 0_i64))
983 .stream(10);
984
985 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
986
987 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
988
989 assert_eq!(chunks.len(), 1);
991 assert_eq!(chunks[0].rows.len(), 0);
992 assert!(chunks[0].is_last);
993 }
994
995 #[test]
996 fn sqlite_fetch_stream_exact_chunk_boundary() {
997 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(Connection::open_in_memory().unwrap())));
998 let entity = entity();
999
1000 executor
1001 .execute(&CompiledQuery {
1002 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT)"
1003 .to_owned(),
1004 params: Vec::new(),
1005 comment: None,
1006 })
1007 .unwrap();
1008
1009 for i in 1..=20 {
1011 let insert = SqliteDialect
1012 .compile_insert(
1013 &entity,
1014 &InsertCommand::new("Order")
1015 .value("id", i as u64)
1016 .value("version", 1_i64)
1017 .value("name", format!("order-{i}")),
1018 )
1019 .unwrap();
1020 executor.execute(&insert).unwrap();
1021 }
1022
1023 let query = SelectQuery::new("Order")
1024 .filter(Expr::gt("version", 0_i64))
1025 .order_asc("id")
1026 .stream(10);
1027
1028 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1029
1030 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1031
1032 assert_eq!(chunks.len(), 3);
1034 assert_eq!(chunks[0].rows.len(), 10);
1035 assert!(!chunks[0].is_last);
1036 assert_eq!(chunks[1].rows.len(), 10);
1037 assert!(!chunks[1].is_last);
1038 assert_eq!(chunks[2].rows.len(), 0);
1039 assert!(chunks[2].is_last);
1040 }
1041}