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_sql::StreamingSqlTransport for SqliteMutationExecutor {
319 #[allow(clippy::await_holding_lock)]
323 fn stream_sql(
324 &self,
325 query: CompiledQuery,
326 chunk_size: usize,
327 ) -> teaql_data_service::QueryStream<'_, Self::Error> {
328 let connection = self.connection.clone();
329 Box::pin(async_stream::try_stream! {
330 let params = bind_values(&query.params)?;
331 let guard = connection.lock().map_err(|err| MutationExecutorError::Lock(err.to_string()))?;
332 let mut statement = guard.prepare(&query.sql_with_comment())?;
333 let columns = statement_columns(&statement);
334 let mut rows = statement.query(params_from_iter(params.iter()))?;
335 let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
336 while let Some(row) = rows.next()? {
337 chunk.push(decode_sqlite_row(row, &columns)?);
338 if chunk.len() == chunk_size { yield teaql_data_service::StreamChunk { rows: std::mem::take(&mut chunk), chunk_index: index, is_last: false }; index += 1; }
339 }
340 if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows: chunk, chunk_index: index, is_last: true }; }
341 })
342 }
343}
344
345impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
346 fn query_stream(
347 &self,
348 request: teaql_data_service::QueryRequest,
349 chunk_size: usize,
350 ) -> teaql_data_service::QueryStream<'_, Self::Error> {
351 let dialect = SqliteDialect;
352 let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
354 match dialect.compile_select(&entity_desc, &request.query) {
355 Ok(compiled) => {
356 teaql_sql::StreamingSqlTransport::stream_sql(self, compiled, chunk_size)
357 }
358 Err(error) => Box::pin(futures_util::stream::once(async {
359 Err(MutationExecutorError::SqlCompile(error))
360 })),
361 }
362 }
363}
364
365impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
366 type Error = MutationExecutorError;
367
368 async fn commit_sql(self) -> Result<(), Self::Error> {
369 self.commit_transaction()
370 }
371
372 async fn rollback_sql(self) -> Result<(), Self::Error> {
373 self.rollback_transaction()
374 }
375}
376
377impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
378 type Tx<'a>
379 = Self
380 where
381 Self: 'a;
382
383 async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
384 self.begin_transaction()?;
385 Ok(self.clone())
386 }
387}
388
389fn initial_graph_exists_sqlite(
390 executor: &SqliteMutationExecutor,
391 dialect: &SqliteDialect,
392 entity: &EntityDescriptor,
393 graph: &GraphNode,
394) -> Result<bool, MutationExecutorError> {
395 let Some(id) = graph.values.get("id") else {
396 return Ok(false);
397 };
398 let query = dialect.compile_select(
399 entity,
400 &SelectQuery::new(&graph.entity)
401 .project("id")
402 .filter(Expr::eq("id", id.clone()))
403 .limit(1),
404 )?;
405 Ok(!executor.fetch_all(&query)?.is_empty())
406}
407
408fn compile_initial_graph_insert(
409 dialect: &impl SqlDialect,
410 entity: &EntityDescriptor,
411 graph: &GraphNode,
412) -> Result<CompiledQuery, MutationExecutorError> {
413 let mut command = InsertCommand::new(&graph.entity);
414 for (field, value) in &graph.values {
415 command = command.value(field.clone(), value.clone());
416 }
417 dialect.compile_insert(entity, &command).map_err(Into::into)
418}
419
420fn compile_initial_graph_update(
421 dialect: &impl SqlDialect,
422 entity: &EntityDescriptor,
423 graph: &GraphNode,
424) -> Result<Option<CompiledQuery>, MutationExecutorError> {
425 let Some(id) = graph.values.get("id") else {
426 return Ok(None);
427 };
428 let mut command = UpdateCommand::new(&graph.entity, id.clone());
429 for (field, value) in &graph.values {
430 if field != "id" {
431 command = command.value(field.clone(), value.clone());
432 }
433 }
434 match dialect.compile_update(entity, &command) {
435 Ok(query) => Ok(Some(query)),
436 Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
437 Err(err) => Err(err.into()),
438 }
439}
440
441pub trait SqliteSchemaExt {
442 fn ensure_sqlite_schema(
443 &self,
444 ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>>;
445}
446
447pub fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
448 let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
449 MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
450 })?;
451 let executor = context
452 .get_resource::<SqliteMutationExecutor>()
453 .ok_or_else(|| {
454 MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
455 })?;
456
457 let entities = context.all_entities();
458
459 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
461
462 for entity in &entities {
464 let field_count = entity.properties.len();
465 if !executor.table_exists(&entity.table_name)? {
466 let sql = dialect.compile_create_table(entity)?;
468 executor.lock()?.execute(&sql, [])?;
469 let _ = context.send_event(RawAuditEvent::schema_created(
470 &entity.name,
471 &entity.table_name,
472 field_count,
473 ));
474 continue;
475 }
476 let existing_columns = executor.table_columns(&entity.table_name)?;
478 let mut fields_added = 0;
479 for property in &entity.properties {
480 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
481 if existing_columns.contains(&bare_column) {
482 continue;
483 }
484 let sql = dialect.compile_add_column(entity, property)?;
485 executor.lock()?.execute(&sql, [])?;
486 let _ = context.send_event(RawAuditEvent::field_added(
487 &entity.name,
488 &entity.table_name,
489 &property.column_name,
490 ));
491 fields_added += 1;
492 }
493 let _ = context.send_event(RawAuditEvent::schema_verified(
494 &entity.name,
495 &entity.table_name,
496 field_count,
497 ));
498 let _ = fields_added; }
500
501 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); for graph in context.initial_graphs() {
504 let entity = context.entity(&graph.entity).ok_or_else(|| {
505 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
506 })?;
507 let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
508 if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
509 if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
510 executor.execute(&query)?;
511 }
512 counts.1 += 1;
513 continue;
514 }
515 let query = compile_initial_graph_insert(dialect, entity, graph)?;
516 executor.execute(&query)?;
517 counts.0 += 1; }
519
520 for graph in context.root_graphs() {
522 let entity = context.entity(&graph.entity).ok_or_else(|| {
523 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
524 })?;
525 if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
526 continue;
527 }
528 let query = compile_initial_graph_insert(dialect, entity, graph)?;
529 executor.execute(&query)?;
530 seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
531 }
532
533 for (entity_name, (inserted, updated)) in &seed_counts {
535 let entity = context.entity(entity_name).ok_or_else(|| {
536 MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
537 })?;
538 let _ = context.send_event(RawAuditEvent::data_seeded(
539 entity_name,
540 &entity.table_name,
541 *inserted,
542 *updated,
543 ));
544 }
545
546 Ok(())
547}
548
549impl SqliteSchemaExt for UserContext {
550 fn ensure_sqlite_schema(
551 &self,
552 ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>> {
553 Box::pin(async move { ensure_sqlite_schema_for(self) })
554 }
555}
556
557#[derive(Debug, Default, Clone, Copy)]
558pub struct SqliteSchemaProvider;
559
560impl SchemaProvider for SqliteSchemaProvider {
561 fn ensure_schema<'a>(
562 &'a self,
563 context: &'a UserContext,
564 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
565 Box::pin(async move {
566 ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
567 })
568 }
569}
570
571pub trait SqliteProviderExt {
572 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
573}
574
575impl SqliteProviderExt for UserContext {
576 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
577 self.insert_resource(SqliteDialect);
578 self.insert_resource(executor);
579 self.set_schema_provider(SqliteSchemaProvider);
580 self
581 }
582}
583
584#[derive(Clone)]
585pub struct SqliteIdSpaceGenerator {
586 executor: SqliteMutationExecutor,
587 table_name: String,
588}
589
590impl SqliteIdSpaceGenerator {
591 pub fn new(connection: Connection) -> Self {
592 Self::from_executor(SqliteMutationExecutor::from_connection(connection))
593 }
594
595 pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
596 Self {
597 executor,
598 table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
599 }
600 }
601
602 pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
603 self.table_name = table_name.into();
604 self
605 }
606
607 pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
608 self.executor.ensure_id_space_table(&self.table_name)
609 }
610
611 pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
612 self.ensure_table()?;
613 let sql = format!(
614 "INSERT INTO {} (type_name, current_level) VALUES (?, 1) \
615 ON CONFLICT (type_name) DO UPDATE \
616 SET current_level = current_level + 1 \
617 RETURNING current_level",
618 quote_ident(&self.table_name)
619 );
620 let id: i64 = self
621 .executor
622 .lock()?
623 .query_row(&sql, [entity], |row| row.get(0))?;
624 u64::try_from(id).map_err(|_| {
625 MutationExecutorError::Bind(format!("generated id {id} cannot be represented as u64"))
626 })
627 }
628}
629
630impl InternalIdGenerator for SqliteIdSpaceGenerator {
631 fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
632 self.next_id(entity)
633 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
634 }
635}
636
637fn quote_ident(ident: &str) -> String {
638 quote_identifier_if_needed(ident, '"')
639}
640
641fn strip_identifier_quotes(ident: &str) -> &str {
649 let bytes = ident.as_bytes();
650 if bytes.len() >= 2 {
651 let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
652 if (first == b'"' && last == b'"')
653 || (first == b'`' && last == b'`')
654 || (first == b'[' && last == b']')
655 {
656 return &ident[1..ident.len() - 1];
657 }
658 }
659 ident
660}
661
662fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
663 values.iter().map(bind_sqlite_value).collect()
664}
665
666fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
667 match value {
668 Value::Null => Ok(SqliteValue::Null),
669 Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
670 Value::I64(v) => Ok(SqliteValue::Integer(*v)),
671 Value::U64(v) => i64::try_from(*v)
672 .map(SqliteValue::Integer)
673 .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
674 Value::F64(v) => Ok(SqliteValue::Real(*v)),
675 Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
679 Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
680 Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
681 Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
682 Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
683 Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
684 Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
685 Value::TypedNull(_) => Ok(SqliteValue::Null),
686 }
687}
688
689#[derive(Debug, Clone)]
690struct ColumnInfo {
691 name: String,
692 decl_type: Option<String>,
693}
694
695fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
696 statement
697 .columns()
698 .into_iter()
699 .map(|column| ColumnInfo {
700 name: column.name().to_owned(),
701 decl_type: column.decl_type().map(|value| value.to_ascii_uppercase()),
702 })
703 .collect()
704}
705
706fn decode_sqlite_row(
707 row: &Row<'_>,
708 columns: &[ColumnInfo],
709) -> Result<Record, MutationExecutorError> {
710 let mut record = BTreeMap::new();
711 for (index, column) in columns.iter().enumerate() {
712 let value_ref = row.get_ref(index)?;
713 let value = match value_ref {
714 ValueRef::Null => Value::Null,
715 ValueRef::Integer(value) => decode_sqlite_integer(value, column),
716 ValueRef::Real(value) => Value::F64(value),
717 ValueRef::Text(value) => decode_sqlite_text(value, column)?,
718 ValueRef::Blob(_) => {
719 return Err(MutationExecutorError::UnsupportedColumnType(
720 "BLOB".to_owned(),
721 ));
722 }
723 };
724 record.insert(column.name.clone(), value);
725 }
726 Ok(record)
727}
728
729fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
730 match column_decl_type(column).as_deref() {
731 Some("BOOLEAN") | Some("BOOL") => Value::Bool(value != 0),
732 _ => Value::I64(value),
733 }
734}
735
736fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
737 let value = std::str::from_utf8(value)
738 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
739 match column_decl_type(column).as_deref() {
740 Some("NUMERIC") | Some("DECIMAL") => Decimal::from_str(value)
741 .map(Value::Decimal)
742 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
743 Some("JSON") => serde_json::from_str(value).map(Value::Json).map_err(|err| {
744 MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
745 }),
746 Some("DATE") => NaiveDate::parse_from_str(value, "%Y-%m-%d")
747 .map(Value::Date)
748 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
749 Some("TIMESTAMP") | Some("DATETIME") => parse_sqlite_timestamp(value),
750 _ => infer_sqlite_text(value),
751 }
752}
753
754fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
755 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
756 return Ok(Value::Date(date));
757 }
758 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
759 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
760 timestamp.timestamp_millis(),
761 )));
762 }
763 if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
764 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
765 timestamp.and_utc().timestamp_millis(),
766 )));
767 }
768 Ok(Value::Text(value.to_owned()))
769}
770
771fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
772 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
773 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
774 timestamp.timestamp_millis(),
775 )));
776 }
777 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
778 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
779 date.and_hms_opt(0, 0, 0)
780 .unwrap_or_default()
781 .and_utc()
782 .timestamp_millis(),
783 )));
784 }
785 NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
786 .map(|timestamp| {
787 Value::Timestamp(teaql_core::time::Timestamp(
788 timestamp.and_utc().timestamp_millis(),
789 ))
790 })
791 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
792}
793
794fn column_decl_type(column: &ColumnInfo) -> Option<String> {
795 column
796 .decl_type
797 .as_ref()
798 .map(|value| value.split('(').next().unwrap_or(value).trim().to_owned())
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use futures_util::StreamExt;
805 use teaql_core::{DeleteCommand, RecoverCommand};
806 use teaql_macros::TeaqlEntity;
807 use teaql_runtime::InMemoryMetadataStore;
808
809 #[test]
810 fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
811 let connection = Connection::open_in_memory().unwrap();
812 connection
813 .execute_batch(
814 "CREATE TABLE stream_fixture(id INTEGER);\
815 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
816 )
817 .unwrap();
818 let executor = SqliteMutationExecutor::from_connection(connection);
819 let query = CompiledQuery {
820 sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
821 params: vec![],
822 comment: None,
823 };
824 let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
825 let sizes = futures_executor::block_on(async {
826 let mut result = Vec::new();
827 while let Some(chunk) = stream.next().await {
828 result.push(chunk.unwrap().rows.len());
829 }
830 result
831 });
832 assert_eq!(sizes, vec![2, 2, 1]);
833
834 let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
835 assert_eq!(
836 futures_executor::block_on(early.next())
837 .unwrap()
838 .unwrap()
839 .rows
840 .len(),
841 2
842 );
843 drop(early);
844 let count: i64 = executor
845 .connection()
846 .lock()
847 .unwrap()
848 .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
849 .unwrap();
850 assert_eq!(count, 5);
851 }
852
853 #[test]
854 fn decimal_bind_is_numeric_and_comparable() {
855 let value =
856 bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
857 assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
858 let connection = Connection::open_in_memory().unwrap();
859 let matches: i64 = connection
860 .query_row(
861 "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
862 [value],
863 |row| row.get(0),
864 )
865 .unwrap();
866 assert_eq!(matches, 1);
867 }
868
869 #[test]
870 fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
871 let connection = Connection::open_in_memory().unwrap();
872 connection
873 .execute_batch(
874 "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
875 )
876 .unwrap();
877 let query = CompiledQuery {
878 sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
879 params: vec![
880 Value::I64(1),
881 Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
882 Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
883 ],
884 comment: None,
885 };
886 let values = bind_values(&query.params).unwrap();
887 connection
888 .execute(&query.sql, rusqlite::params_from_iter(values))
889 .unwrap();
890 connection
891 .execute(
892 &query
893 .debug_sql(teaql_sql::DatabaseKind::Sqlite)
894 .replace("VALUES (1,", "VALUES (2,"),
895 [],
896 )
897 .unwrap();
898
899 let equal_count: i64 = connection.query_row(
900 "SELECT count(*) FROM temporal_fixture a JOIN temporal_fixture b ON a.d=b.d AND a.t=b.t WHERE a.id=1 AND b.id=2",
901 [], |row| row.get(0),
902 ).unwrap();
903 let storage_type: String = connection
904 .query_row(
905 "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
906 [],
907 |row| row.get(0),
908 )
909 .unwrap();
910 assert_eq!(equal_count, 1);
911 assert_eq!(storage_type, "integer");
912 }
913
914 fn entity() -> EntityDescriptor {
915 EntityDescriptor::new("Order")
916 .table_name("orders")
917 .property(
918 PropertyDescriptor::new("id", DataType::U64)
919 .column_name("id")
920 .id()
921 .not_null(),
922 )
923 .property(
924 PropertyDescriptor::new("version", DataType::I64)
925 .column_name("version")
926 .version()
927 .not_null(),
928 )
929 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
930 }
931
932 fn order_line_entity() -> EntityDescriptor {
933 EntityDescriptor::new("OrderLine")
934 .table_name("order_line")
935 .property(
936 PropertyDescriptor::new("id", DataType::U64)
937 .column_name("id")
938 .id()
939 .not_null(),
940 )
941 .property(
942 PropertyDescriptor::new("order_id", DataType::U64)
943 .column_name("order_id")
944 .not_null(),
945 )
946 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
947 }
948
949 #[allow(dead_code)]
950 #[derive(Debug, PartialEq, TeaqlEntity)]
951 #[teaql(entity = "FeatureFlag", table = "feature_flags")]
952 struct FeatureFlagRow {
953 #[teaql(id)]
954 id: u64,
955 #[teaql(version)]
956 version: i64,
957 enabled: bool,
958 optional_enabled: Option<bool>,
959 }
960
961 fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
962 Record::from([
963 ("id".to_owned(), Value::U64(1)),
964 ("version".to_owned(), Value::I64(1)),
965 ("enabled".to_owned(), enabled),
966 ("optional_enabled".to_owned(), optional_enabled),
967 ])
968 }
969
970 #[test]
971 fn sqlite_dialect_compiles_mutations_and_schema() {
972 let insert = SqliteDialect
973 .compile_insert(
974 &entity(),
975 &InsertCommand::new("Order")
976 .value("id", 1_u64)
977 .value("name", "A"),
978 )
979 .unwrap();
980 assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
981
982 let update = SqliteDialect
983 .compile_update(
984 &entity(),
985 &UpdateCommand::new("Order", 1_u64)
986 .expected_version(3)
987 .value("name", "B"),
988 )
989 .unwrap();
990 assert_eq!(
991 update.sql,
992 "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
993 );
994
995 let delete = SqliteDialect
996 .compile_delete(
997 &entity(),
998 &DeleteCommand::new("Order", 1_u64).expected_version(3),
999 )
1000 .unwrap();
1001 let recover = SqliteDialect
1002 .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1003 .unwrap();
1004 assert_eq!(
1005 delete.sql,
1006 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1007 );
1008 assert_eq!(
1009 recover.sql,
1010 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1011 );
1012
1013 let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1014 assert_eq!(
1015 create,
1016 "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1017 );
1018 }
1019
1020 #[test]
1021 fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1022 let executor =
1023 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1024 let entity = entity();
1025 let mut context = UserContext::new()
1026 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1027
1028 context.use_sqlite_provider(executor.clone());
1029 ensure_sqlite_schema_for(&context).unwrap();
1030
1031 let insert = SqliteDialect
1032 .compile_insert(
1033 &entity,
1034 &InsertCommand::new("Order")
1035 .value("id", 1_u64)
1036 .value("version", 1_i64)
1037 .value("name", "draft"),
1038 )
1039 .unwrap();
1040 assert_eq!(executor.execute(&insert).unwrap(), 1);
1041
1042 let select = SqliteDialect
1043 .compile_select(
1044 &entity,
1045 &SelectQuery::new("Order")
1046 .filter(Expr::eq("id", 1_u64))
1047 .order_asc("id"),
1048 )
1049 .unwrap();
1050 let rows = executor.fetch_all(&select).unwrap();
1051 assert_eq!(rows.len(), 1);
1052 assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1053 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1054 assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
1055 }
1056
1057 #[test]
1058 fn repeated_schema_ensure_does_not_overwrite_existing_initial_graph() {
1059 let executor =
1060 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1061 let entity = entity();
1062 let mut context = UserContext::new()
1063 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1064 context.set_root_graphs(vec![
1065 GraphNode::new("Order")
1066 .value("id", 1_u64)
1067 .value("version", 1_i64)
1068 .value("name", "module seed"),
1069 ]);
1070 context.use_sqlite_provider(executor.clone());
1071
1072 ensure_sqlite_schema_for(&context).unwrap();
1073 let customize = SqliteDialect
1074 .compile_update(
1075 &entity,
1076 &UpdateCommand::new("Order", 1_u64).value("name", "application value"),
1077 )
1078 .unwrap();
1079 assert_eq!(executor.execute(&customize).unwrap(), 1);
1080
1081 ensure_sqlite_schema_for(&context).unwrap();
1082
1083 let select = SqliteDialect
1084 .compile_select(
1085 &entity,
1086 &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1087 )
1088 .unwrap();
1089 let rows = executor.fetch_all(&select).unwrap();
1090 assert_eq!(rows.len(), 1);
1091 assert_eq!(
1092 rows[0].get("name"),
1093 Some(&Value::Text("application value".to_owned()))
1094 );
1095 }
1096
1097 #[test]
1098 fn repeated_schema_ensure_reconciles_changed_constant_graph() {
1099 let executor =
1100 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1101 let entity = entity();
1102 let mut context = UserContext::new()
1103 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1104 context.set_initial_graphs(vec![
1105 GraphNode::new("Order")
1106 .value("id", 1_u64)
1107 .value("version", 1_i64)
1108 .value("name", "red"),
1109 ]);
1110 context.use_sqlite_provider(executor.clone());
1111 ensure_sqlite_schema_for(&context).unwrap();
1112
1113 context.set_initial_graphs(vec![
1114 GraphNode::new("Order")
1115 .value("id", 1_u64)
1116 .value("version", 1_i64)
1117 .value("name", "crimson"),
1118 ]);
1119 ensure_sqlite_schema_for(&context).unwrap();
1120
1121 let select = SqliteDialect
1122 .compile_select(
1123 &entity,
1124 &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1125 )
1126 .unwrap();
1127 let rows = executor.fetch_all(&select).unwrap();
1128 assert_eq!(
1129 rows[0].get("name"),
1130 Some(&Value::Text("crimson".to_owned()))
1131 );
1132 }
1133
1134 #[test]
1135 fn sqlite_executes_partitioned_relation_limit_per_parent() {
1136 let executor =
1137 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1138 let entity = order_line_entity();
1139 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1140
1141 for order_id in [11_u64, 12_u64] {
1142 for index in 1_u64..=5 {
1143 let id = order_id * 100 + index;
1144 let insert = SqliteDialect
1145 .compile_insert(
1146 &entity,
1147 &InsertCommand::new("OrderLine")
1148 .value("id", id)
1149 .value("order_id", order_id)
1150 .value("name", format!("line-{id}")),
1151 )
1152 .unwrap();
1153 executor.execute(&insert).unwrap();
1154 }
1155 }
1156
1157 let query = SelectQuery::new("OrderLine")
1158 .project("id")
1159 .project("order_id")
1160 .order_desc("id")
1161 .limit(3)
1162 .partition_by("order_id");
1163 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1164 let rows = executor.fetch_all(&compiled).unwrap();
1165
1166 assert_eq!(rows.len(), 6);
1167 for order_id in [11_i64, 12_i64] {
1168 let ids = rows
1169 .iter()
1170 .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
1171 .filter_map(|row| row.get("id").cloned())
1172 .collect::<Vec<_>>();
1173 assert_eq!(
1174 ids,
1175 vec![
1176 Value::I64(order_id * 100 + 5),
1177 Value::I64(order_id * 100 + 4),
1178 Value::I64(order_id * 100 + 3),
1179 ]
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn sqlite_boolean_new_schema_roundtrips_as_bool() {
1186 let executor =
1187 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1188 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1189 let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
1190 assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
1191 assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
1192 assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
1193
1194 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1195 for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
1196 let insert = SqliteDialect
1197 .compile_insert(
1198 &entity,
1199 &InsertCommand::new("FeatureFlag")
1200 .value("id", id)
1201 .value("version", 1_i64)
1202 .value("enabled", enabled)
1203 .value("optional_enabled", optional_enabled),
1204 )
1205 .unwrap();
1206 assert_eq!(executor.execute(&insert).unwrap(), 1);
1207 }
1208
1209 let select = SqliteDialect
1210 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1211 .unwrap();
1212 let rows = executor.fetch_all(&select).unwrap();
1213 assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
1214 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
1215 assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
1216 assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
1217
1218 let first = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[0].clone()).unwrap();
1219 let second = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[1].clone()).unwrap();
1220 assert!(!first.enabled);
1221 assert_eq!(first.optional_enabled, Some(true));
1222 assert!(second.enabled);
1223 assert_eq!(second.optional_enabled, Some(false));
1224 }
1225
1226 #[test]
1227 fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
1228 let executor =
1229 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1230 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1231 executor
1232 .execute(&CompiledQuery {
1233 sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
1234 .to_owned(),
1235 params: Vec::new(),
1236 comment: None,
1237 })
1238 .unwrap();
1239
1240 let insert = SqliteDialect
1241 .compile_insert(
1242 &entity,
1243 &InsertCommand::new("FeatureFlag")
1244 .value("id", 1_u64)
1245 .value("version", 1_i64)
1246 .value("enabled", true)
1247 .value("optional_enabled", false),
1248 )
1249 .unwrap();
1250 executor.execute(&insert).unwrap();
1251 executor
1252 .execute(&CompiledQuery {
1253 sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
1254 .to_owned(),
1255 params: vec![
1256 Value::U64(2),
1257 Value::I64(1),
1258 Value::I64(2),
1259 Value::Null,
1260 ],
1261 comment: None,
1262 })
1263 .unwrap();
1264 let select = SqliteDialect
1265 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1266 .unwrap();
1267 let rows = executor.fetch_all(&select).unwrap();
1268 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1269 assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
1270 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
1271
1272 let decoded = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[0].clone()).unwrap();
1273 assert!(decoded.enabled);
1274 assert_eq!(decoded.optional_enabled, Some(false));
1275 assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
1276 let error =
1277 <FeatureFlagRow as teaql_core::Entity>::from_record(rows[1].clone()).unwrap_err();
1278 assert!(error.message.contains("invalid field enabled"));
1279
1280 for (value, expected) in [
1281 (Value::I64(0), false),
1282 (Value::I64(1), true),
1283 (Value::U64(0), false),
1284 (Value::U64(1), true),
1285 ] {
1286 let decoded = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1287 value,
1288 Value::Null,
1289 ))
1290 .unwrap();
1291 assert_eq!(decoded.enabled, expected);
1292 assert_eq!(decoded.optional_enabled, None);
1293 }
1294
1295 for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
1296 let error = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1297 invalid,
1298 Value::Null,
1299 ))
1300 .unwrap_err();
1301 assert!(error.message.contains("invalid field enabled"));
1302 }
1303 let error = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1304 Value::Bool(true),
1305 Value::U64(2),
1306 ))
1307 .unwrap_err();
1308 assert!(error.message.contains("invalid field optional_enabled"));
1309 }
1310
1311 #[test]
1312 fn sqlite_executor_parses_json_only_for_json_columns() {
1313 let executor =
1314 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1315
1316 executor
1317 .execute(&CompiledQuery {
1318 sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
1319 params: Vec::new(),
1320 comment: None,
1321 })
1322 .unwrap();
1323 executor
1324 .execute(&CompiledQuery {
1325 sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
1326 params: vec![
1327 Value::Text("{\"active\":true}".to_owned()),
1328 Value::Json(serde_json::json!({"active": true})),
1329 ],
1330 comment: None,
1331 })
1332 .unwrap();
1333
1334 let rows = executor
1335 .fetch_all(&CompiledQuery {
1336 sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
1337 params: Vec::new(),
1338 comment: None,
1339 })
1340 .unwrap();
1341
1342 assert_eq!(
1343 rows[0].get("text_payload"),
1344 Some(&Value::Text("{\"active\":true}".to_owned()))
1345 );
1346 assert_eq!(
1347 rows[0].get("json_payload"),
1348 Some(&Value::Json(serde_json::json!({"active": true})))
1349 );
1350 }
1351
1352 #[test]
1353 fn sqlite_id_space_generator_increments_ids() {
1354 let executor =
1355 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1356 let generator = SqliteIdSpaceGenerator::from_executor(executor);
1357 assert_eq!(generator.next_id("Order").unwrap(), 1);
1358 assert_eq!(generator.next_id("Order").unwrap(), 2);
1359 }
1360
1361 #[test]
1362 fn sqlite_fetch_stream_returns_chunked_rows() {
1363 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1364 Connection::open_in_memory().unwrap(),
1365 )));
1366 let entity = entity();
1367
1368 executor
1370 .execute(&CompiledQuery {
1371 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1372 .to_owned(),
1373 params: Vec::new(),
1374 comment: None,
1375 })
1376 .unwrap();
1377
1378 for i in 1..=25 {
1379 let insert = SqliteDialect
1380 .compile_insert(
1381 &entity,
1382 &InsertCommand::new("Order")
1383 .value("id", i as u64)
1384 .value("version", 1_i64)
1385 .value("name", format!("order-{i}")),
1386 )
1387 .unwrap();
1388 executor.execute(&insert).unwrap();
1389 }
1390
1391 let query = SelectQuery::new("Order")
1393 .filter(Expr::gt("version", 0_i64))
1394 .order_asc("id")
1395 .stream(10);
1396
1397 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1398
1399 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1400
1401 assert_eq!(chunks.len(), 3);
1403 assert_eq!(chunks[0].rows.len(), 10);
1404 assert_eq!(chunks[0].chunk_index, 0);
1405 assert!(!chunks[0].is_last);
1406
1407 assert_eq!(chunks[1].rows.len(), 10);
1408 assert_eq!(chunks[1].chunk_index, 1);
1409 assert!(!chunks[1].is_last);
1410
1411 assert_eq!(chunks[2].rows.len(), 5);
1412 assert_eq!(chunks[2].chunk_index, 2);
1413 assert!(chunks[2].is_last);
1414
1415 assert_eq!(
1417 chunks[0].rows[0].get("name"),
1418 Some(&Value::Text("order-1".to_owned()))
1419 );
1420 assert_eq!(
1421 chunks[2].rows[4].get("name"),
1422 Some(&Value::Text("order-25".to_owned()))
1423 );
1424 }
1425
1426 #[test]
1427 fn sqlite_fetch_stream_handles_empty_result() {
1428 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1429 Connection::open_in_memory().unwrap(),
1430 )));
1431
1432 executor
1433 .execute(&CompiledQuery {
1434 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1435 .to_owned(),
1436 params: Vec::new(),
1437 comment: None,
1438 })
1439 .unwrap();
1440
1441 let entity = entity();
1442 let query = SelectQuery::new("Order")
1443 .filter(Expr::gt("version", 0_i64))
1444 .stream(10);
1445
1446 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1447
1448 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1449
1450 assert_eq!(chunks.len(), 1);
1452 assert_eq!(chunks[0].rows.len(), 0);
1453 assert!(chunks[0].is_last);
1454 }
1455
1456 #[test]
1457 fn sqlite_fetch_stream_exact_chunk_boundary() {
1458 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1459 Connection::open_in_memory().unwrap(),
1460 )));
1461 let entity = entity();
1462
1463 executor
1464 .execute(&CompiledQuery {
1465 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1466 .to_owned(),
1467 params: Vec::new(),
1468 comment: None,
1469 })
1470 .unwrap();
1471
1472 for i in 1..=20 {
1474 let insert = SqliteDialect
1475 .compile_insert(
1476 &entity,
1477 &InsertCommand::new("Order")
1478 .value("id", i as u64)
1479 .value("version", 1_i64)
1480 .value("name", format!("order-{i}")),
1481 )
1482 .unwrap();
1483 executor.execute(&insert).unwrap();
1484 }
1485
1486 let query = SelectQuery::new("Order")
1487 .filter(Expr::gt("version", 0_i64))
1488 .order_asc("id")
1489 .stream(10);
1490
1491 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1492
1493 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1494
1495 assert_eq!(chunks.len(), 3);
1497 assert_eq!(chunks[0].rows.len(), 10);
1498 assert!(!chunks[0].is_last);
1499 assert_eq!(chunks[1].rows.len(), 10);
1500 assert!(!chunks[1].is_last);
1501 assert_eq!(chunks[2].rows.len(), 0);
1502 assert!(chunks[2].is_last);
1503 }
1504
1505 #[test]
1506 fn test_parse_sqlite_timestamp() {
1507 let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
1508 assert!(matches!(ts1, Value::Timestamp(_)));
1509
1510 let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
1511 assert!(matches!(ts2, Value::Timestamp(_)));
1512
1513 let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
1514 assert!(matches!(ts3, Value::Timestamp(_)));
1515
1516 assert!(parse_sqlite_timestamp("invalid").is_err());
1517 }
1518}