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