1use std::collections::BTreeMap;
2
3use rusqlite::Connection;
4use serde::Serialize;
5
6use crate::{
7 ColumnSnapshot, FieldSpec, IndexSnapshot, SchemaOperation, SchemaPlan, SchemaSnapshot,
8 StorageAdapter, StorageError, TableSnapshot,
9};
10use pylon_kernel::AppManifest;
11
12fn sqlite_column_type(field_type: &str) -> &'static str {
25 match field_type {
26 "string" => "TEXT",
27 "int" => "INTEGER",
28 "float" => "REAL",
29 "bool" => "INTEGER",
30 "datetime" => "TEXT",
31 "richtext" => "TEXT",
32 _ if field_type.starts_with("id(") => "TEXT",
33 _ => "TEXT",
34 }
35}
36
37fn quote_ident(name: &str) -> String {
44 format!("\"{}\"", name.replace('"', "\"\""))
45}
46
47pub fn create_table_sql(entity_name: &str, fields: &[FieldSpec]) -> String {
53 let mut columns = vec!["id TEXT PRIMARY KEY NOT NULL".to_string()];
54
55 for field in fields {
56 let col_type = sqlite_column_type(&field.field_type);
57 let not_null = if field.optional { "" } else { " NOT NULL" };
58 let unique = if field.unique { " UNIQUE" } else { "" };
59 columns.push(format!(
60 "{} {}{}{}",
61 quote_ident(&field.name),
62 col_type,
63 not_null,
64 unique
65 ));
66 }
67
68 format!(
69 "CREATE TABLE IF NOT EXISTS {} ({})",
70 quote_ident(entity_name),
71 columns.join(", ")
72 )
73}
74
75pub fn add_column_sql(entity_name: &str, field: &FieldSpec) -> String {
77 let col_type = sqlite_column_type(&field.field_type);
78 let unique = if field.unique { " UNIQUE" } else { "" };
82 format!(
83 "ALTER TABLE {} ADD COLUMN {} {}{}",
84 quote_ident(entity_name),
85 quote_ident(&field.name),
86 col_type,
87 unique,
88 )
89}
90
91pub fn create_index_sql(
93 entity_name: &str,
94 index_name: &str,
95 fields: &[String],
96 unique: bool,
97) -> String {
98 let unique_str = if unique { "UNIQUE " } else { "" };
99 let full_index_name = format!("{}_{}", entity_name, index_name);
100 let quoted_fields: Vec<String> = fields.iter().map(|f| quote_ident(f)).collect();
101 format!(
102 "CREATE {}INDEX IF NOT EXISTS {} ON {} ({})",
103 unique_str,
104 quote_ident(&full_index_name),
105 quote_ident(entity_name),
106 quoted_fields.join(", ")
107 )
108}
109
110pub struct SqliteAdapter {
115 conn: Connection,
116}
117
118impl SqliteAdapter {
119 pub fn open(path: &str) -> Result<Self, StorageError> {
121 let conn = Connection::open(path).map_err(|e| StorageError {
122 code: "SQLITE_OPEN_FAILED".into(),
123 message: format!("Failed to open SQLite database at {path}: {e}"),
124 })?;
125 tune_connection(&conn, false)?;
126 Ok(Self { conn })
127 }
128
129 pub fn in_memory() -> Result<Self, StorageError> {
131 let conn = Connection::open_in_memory().map_err(|e| StorageError {
132 code: "SQLITE_OPEN_FAILED".into(),
133 message: format!("Failed to open in-memory SQLite database: {e}"),
134 })?;
135 tune_connection(&conn, true)?;
136 Ok(Self { conn })
137 }
138}
139
140fn tune_connection(conn: &Connection, in_memory: bool) -> Result<(), StorageError> {
171 let pragmas: &[(&str, &str)] = if in_memory {
172 &[
173 ("temp_store", "MEMORY"),
174 ("cache_size", "-65536"),
175 ("foreign_keys", "ON"),
176 ]
177 } else {
178 &[
179 ("journal_mode", "WAL"),
180 ("synchronous", "NORMAL"),
181 ("cache_size", "-65536"),
182 ("mmap_size", "268435456"),
183 ("temp_store", "MEMORY"),
184 ("busy_timeout", "5000"),
185 ("foreign_keys", "ON"),
186 ("wal_autocheckpoint", "1000"),
191 ]
192 };
193 for (key, value) in pragmas {
194 conn.pragma_update(None, key, value)
195 .map_err(|e| StorageError {
196 code: "SQLITE_PRAGMA_FAILED".into(),
197 message: format!("PRAGMA {key}={value} failed: {e}"),
198 })?;
199 }
200 Ok(())
201}
202
203impl SqliteAdapter {
204 pub fn plan_from_live(&self, target: &AppManifest) -> Result<SchemaPlan, StorageError> {
207 let snapshot = self.read_schema()?;
208 Ok(crate::plan_from_snapshot(&snapshot, target))
209 }
210}
211
212impl StorageAdapter for SqliteAdapter {
213 fn plan_schema(&self, target: &AppManifest) -> Result<SchemaPlan, StorageError> {
214 self.plan_from_live(target)
216 }
217
218 fn apply_schema(&self, plan: &SchemaPlan) -> Result<(), StorageError> {
219 self.conn.execute("BEGIN", []).map_err(|e| StorageError {
224 code: "SQLITE_EXEC_FAILED".into(),
225 message: format!("BEGIN failed: {e}"),
226 })?;
227 match self.apply_schema_impl(plan) {
228 Ok(()) => {
229 self.conn.execute("COMMIT", []).map_err(|e| StorageError {
230 code: "SQLITE_EXEC_FAILED".into(),
231 message: format!("COMMIT failed after apply: {e}"),
232 })?;
233 Ok(())
234 }
235 Err(e) => {
236 if let Err(rb) = self.conn.execute("ROLLBACK", []) {
237 tracing::warn!("[sqlite] ROLLBACK after apply error failed: {rb}");
241 }
242 Err(e)
243 }
244 }
245 }
246}
247
248const HISTORY_TABLE: &str = "_pylon_schema_history";
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
256pub struct HistoryEntry {
257 pub id: String,
258 pub manifest_version: i64,
259 pub app_version: String,
260 pub applied_at: String,
261 pub operation_count: i64,
262 pub baseline: String,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub plan: Option<SchemaPlan>,
265 pub plan_json: String,
266}
267
268pub struct PushMetadata<'a> {
270 pub manifest_version: u32,
271 pub app_version: &'a str,
272 pub baseline: &'a str,
273}
274
275impl SqliteAdapter {
276 fn ensure_history_table(&self) -> Result<(), StorageError> {
278 let sql = format!(
279 "CREATE TABLE IF NOT EXISTS {} (\
280 id TEXT PRIMARY KEY NOT NULL, \
281 manifest_version INTEGER NOT NULL, \
282 app_version TEXT NOT NULL, \
283 applied_at TEXT NOT NULL, \
284 operation_count INTEGER NOT NULL, \
285 baseline TEXT NOT NULL, \
286 plan_json TEXT NOT NULL\
287 )",
288 quote_ident(HISTORY_TABLE)
289 );
290 self.conn.execute(&sql, []).map_err(|e| StorageError {
291 code: "SQLITE_EXEC_FAILED".into(),
292 message: format!("Failed to create history table: {e}"),
293 })?;
294 Ok(())
295 }
296
297 pub fn apply_with_history(
303 &self,
304 plan: &SchemaPlan,
305 meta: &PushMetadata<'_>,
306 ) -> Result<(), StorageError> {
307 self.ensure_history_table()?;
311
312 self.conn.execute("BEGIN", []).map_err(|e| StorageError {
313 code: "SQLITE_EXEC_FAILED".into(),
314 message: format!("BEGIN failed: {e}"),
315 })?;
316
317 let result = (|| -> Result<(), StorageError> {
318 self.apply_schema_impl(plan)?;
319
320 let plan_json = serde_json::to_string(plan).map_err(|e| StorageError {
321 code: "SQLITE_SERIALIZE_FAILED".into(),
322 message: format!("Failed to serialize plan: {e}"),
323 })?;
324
325 let id = generate_push_id();
326 let now = now_iso8601();
327 let op_count = plan
328 .operations
329 .iter()
330 .filter(|op| !matches!(op, SchemaOperation::Noop))
331 .count() as i64;
332
333 self.conn
334 .execute(
335 &format!(
336 "INSERT INTO {} (id, manifest_version, app_version, applied_at, operation_count, baseline, plan_json) \
337 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
338 quote_ident(HISTORY_TABLE)
339 ),
340 rusqlite::params![
341 id,
342 meta.manifest_version as i64,
343 meta.app_version,
344 now,
345 op_count,
346 meta.baseline,
347 plan_json,
348 ],
349 )
350 .map_err(|e| StorageError {
351 code: "SQLITE_EXEC_FAILED".into(),
352 message: format!("Failed to insert history row: {e}"),
353 })?;
354 Ok(())
355 })();
356
357 match result {
358 Ok(()) => {
359 self.conn.execute("COMMIT", []).map_err(|e| StorageError {
360 code: "SQLITE_EXEC_FAILED".into(),
361 message: format!("COMMIT failed: {e}"),
362 })?;
363 Ok(())
364 }
365 Err(e) => {
366 if let Err(rb) = self.conn.execute("ROLLBACK", []) {
367 tracing::warn!("[sqlite] ROLLBACK after apply_with_history error failed: {rb}");
368 }
369 Err(e)
370 }
371 }
372 }
373
374 pub fn read_history(&self, limit: Option<u32>) -> Result<Vec<HistoryEntry>, StorageError> {
377 if !self.history_table_exists()? {
378 return Ok(Vec::new());
379 }
380
381 let quoted = quote_ident(HISTORY_TABLE);
382 let sql = match limit {
383 Some(n) => format!(
384 "SELECT id, manifest_version, app_version, applied_at, operation_count, baseline, plan_json \
385 FROM {} ORDER BY id DESC LIMIT {}",
386 quoted, n
387 ),
388 None => format!(
389 "SELECT id, manifest_version, app_version, applied_at, operation_count, baseline, plan_json \
390 FROM {} ORDER BY id DESC",
391 quoted
392 ),
393 };
394
395 let mut stmt = self.conn.prepare_cached(&sql).map_err(sqlite_err)?;
396
397 let entries = stmt
398 .query_map([], |row| {
399 let plan_json: String = row.get(6)?;
400 let plan = serde_json::from_str(&plan_json).ok();
401 Ok(HistoryEntry {
402 id: row.get(0)?,
403 manifest_version: row.get(1)?,
404 app_version: row.get(2)?,
405 applied_at: row.get(3)?,
406 operation_count: row.get(4)?,
407 baseline: row.get(5)?,
408 plan,
409 plan_json,
410 })
411 })
412 .map_err(sqlite_err)?
413 .collect::<Result<Vec<_>, _>>()
414 .map_err(sqlite_err)?;
415
416 Ok(entries)
417 }
418
419 pub fn read_history_entry(&self, entry_id: &str) -> Result<Option<HistoryEntry>, StorageError> {
422 if !self.history_table_exists()? {
423 return Ok(None);
424 }
425
426 let mut stmt = self
427 .conn
428 .prepare(&format!(
429 "SELECT id, manifest_version, app_version, applied_at, operation_count, baseline, plan_json \
430 FROM {} WHERE id = ?1",
431 quote_ident(HISTORY_TABLE)
432 ))
433 .map_err(sqlite_err)?;
434
435 let mut rows = stmt
436 .query_map([entry_id], |row| {
437 let plan_json: String = row.get(6)?;
438 let plan = serde_json::from_str(&plan_json).ok();
439 Ok(HistoryEntry {
440 id: row.get(0)?,
441 manifest_version: row.get(1)?,
442 app_version: row.get(2)?,
443 applied_at: row.get(3)?,
444 operation_count: row.get(4)?,
445 baseline: row.get(5)?,
446 plan,
447 plan_json,
448 })
449 })
450 .map_err(sqlite_err)?;
451
452 match rows.next() {
453 Some(Ok(entry)) => Ok(Some(entry)),
454 Some(Err(e)) => Err(sqlite_err(e)),
455 None => Ok(None),
456 }
457 }
458
459 fn history_table_exists(&self) -> Result<bool, StorageError> {
460 let exists: bool = self
461 .conn
462 .query_row(
463 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
464 [HISTORY_TABLE],
465 |row| row.get::<_, i64>(0),
466 )
467 .map_err(sqlite_err)?
468 > 0;
469 Ok(exists)
470 }
471
472 fn apply_schema_impl(&self, plan: &SchemaPlan) -> Result<(), StorageError> {
474 for op in &plan.operations {
475 match op {
476 SchemaOperation::CreateEntity { name, fields } => {
477 let sql = create_table_sql(name, fields);
478 self.conn.execute(&sql, []).map_err(|e| StorageError {
479 code: "SQLITE_EXEC_FAILED".into(),
480 message: format!("Failed to create table {name}: {e}"),
481 })?;
482 }
483 SchemaOperation::AddField { entity, field } => {
484 let sql = add_column_sql(entity, field);
485 self.conn.execute(&sql, []).map_err(|e| StorageError {
486 code: "SQLITE_EXEC_FAILED".into(),
487 message: format!("Failed to add column {}.{}: {e}", entity, field.name),
488 })?;
489 }
490 SchemaOperation::AlterField {
491 entity,
492 previous,
493 target,
494 } => {
495 let _ = (entity, previous, target);
515 tracing::warn!(
516 "[sqlite] AlterField on {entity}.{} requested but SQLite has no DROP/SET NOT NULL — \
517 skipping. Manual table rebuild needed if existing data is incompatible. \
518 (Postgres backend applies the ALTER cleanly; this is an SQLite limitation.)",
519 target.name
520 );
521 }
522 SchemaOperation::AddIndex {
523 entity,
524 name,
525 fields,
526 unique,
527 } => {
528 let sql = create_index_sql(entity, name, fields, *unique);
529 self.conn.execute(&sql, []).map_err(|e| StorageError {
530 code: "SQLITE_EXEC_FAILED".into(),
531 message: format!("Failed to create index {entity}.{name}: {e}"),
532 })?;
533 }
534 SchemaOperation::CreateSearchIndex { entity, config } => {
535 self.conn
539 .execute(crate::search::create_facet_table_sql(), [])
540 .map_err(|e| StorageError {
541 code: "SQLITE_EXEC_FAILED".into(),
542 message: format!("create _facet_bitmap failed: {e}"),
543 })?;
544 if let Some(sql) = crate::search::create_fts_table_sql(entity, config) {
547 self.conn.execute(&sql, []).map_err(|e| StorageError {
548 code: "SQLITE_EXEC_FAILED".into(),
549 message: format!("create _fts_{entity} failed: {e}"),
550 })?;
551 }
552 for field in &config.sortable {
564 let idx_sql = format!(
565 "CREATE INDEX IF NOT EXISTS \"{entity}_sort_{field}\" \
566 ON \"{entity}\" (\"{field}\")"
567 );
568 self.conn.execute(&idx_sql, []).map_err(|e| StorageError {
569 code: "SQLITE_EXEC_FAILED".into(),
570 message: format!("create sort index {entity}.{field} failed: {e}"),
571 })?;
572 }
573 }
574 SchemaOperation::RemoveSearchIndex { entity } => {
575 self.conn
576 .execute(&format!("DROP TABLE IF EXISTS \"_fts_{entity}\""), [])
577 .map_err(|e| StorageError {
578 code: "SQLITE_EXEC_FAILED".into(),
579 message: format!("drop _fts_{entity} failed: {e}"),
580 })?;
581 self.conn
582 .execute("DELETE FROM \"_facet_bitmap\" WHERE entity = ?1", [entity])
583 .map_err(|e| StorageError {
584 code: "SQLITE_EXEC_FAILED".into(),
585 message: format!("clear facet bitmaps for {entity} failed: {e}"),
586 })?;
587 }
588 SchemaOperation::Noop => {}
589 other => {
590 return Err(StorageError {
591 code: "SQLITE_OP_UNSUPPORTED".into(),
592 message: format!("Operation not supported by SQLite adapter: {other:?}"),
593 });
594 }
595 }
596 }
597 Ok(())
598 }
599}
600
601fn generate_push_id() -> String {
602 use std::time::{SystemTime, UNIX_EPOCH};
603 let ts = SystemTime::now()
604 .duration_since(UNIX_EPOCH)
605 .unwrap_or_default();
606 format!("{}.{:09}", ts.as_secs(), ts.subsec_nanos())
607}
608
609fn now_iso8601() -> String {
610 use std::time::{SystemTime, UNIX_EPOCH};
611 let ts = SystemTime::now()
612 .duration_since(UNIX_EPOCH)
613 .unwrap_or_default()
614 .as_secs();
615 let secs_per_day: u64 = 86400;
617 let days = ts / secs_per_day;
618 let rem = ts % secs_per_day;
619 let hours = rem / 3600;
620 let mins = (rem % 3600) / 60;
621 let secs = rem % 60;
622 let (year, month, day) = epoch_days_to_date(days);
624 format!("{year:04}-{month:02}-{day:02}T{hours:02}:{mins:02}:{secs:02}Z")
625}
626
627fn epoch_days_to_date(days: u64) -> (u64, u64, u64) {
628 let z = days + 719468;
630 let era = z / 146097;
631 let doe = z - era * 146097;
632 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
633 let y = yoe + era * 400;
634 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
635 let mp = (5 * doy + 2) / 153;
636 let d = doy - (153 * mp + 2) / 5 + 1;
637 let m = if mp < 10 { mp + 3 } else { mp - 9 };
638 let y = if m <= 2 { y + 1 } else { y };
639 (y, m, d)
640}
641
642impl SqliteAdapter {
647 pub fn read_schema(&self) -> Result<SchemaSnapshot, StorageError> {
650 let mut stmt = self
652 .conn
653 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_pylon_%' ORDER BY name")
654 .map_err(sqlite_err)?;
655
656 let table_names: Vec<String> = stmt
657 .query_map([], |row| row.get(0))
658 .map_err(sqlite_err)?
659 .collect::<Result<Vec<String>, _>>()
660 .map_err(sqlite_err)?;
661
662 let mut tables = Vec::new();
663 for table_name in &table_names {
664 let columns = self.read_columns(table_name)?;
665 let indexes = self.read_indexes(table_name)?;
666 tables.push(TableSnapshot {
667 name: table_name.clone(),
668 columns,
669 indexes,
670 });
671 }
672
673 Ok(SchemaSnapshot { tables })
674 }
675
676 fn read_columns(&self, table: &str) -> Result<Vec<ColumnSnapshot>, StorageError> {
677 let mut stmt = self
678 .conn
679 .prepare(&format!("PRAGMA table_info({})", quote_ident(table)))
680 .map_err(sqlite_err)?;
681
682 let columns: Vec<ColumnSnapshot> = stmt
683 .query_map([], |row| {
684 Ok(ColumnSnapshot {
685 name: row.get(1)?,
686 column_type: row.get(2)?,
687 notnull: row.get::<_, i32>(3)? != 0,
688 primary_key: row.get::<_, i32>(5)? != 0,
689 })
690 })
691 .map_err(sqlite_err)?
692 .collect::<Result<Vec<_>, _>>()
693 .map_err(sqlite_err)?;
694
695 Ok(columns)
696 }
697
698 fn read_indexes(&self, table: &str) -> Result<Vec<IndexSnapshot>, StorageError> {
699 let mut stmt = self
700 .conn
701 .prepare(&format!("PRAGMA index_list({})", quote_ident(table)))
702 .map_err(sqlite_err)?;
703
704 let index_meta: Vec<(String, bool)> = stmt
706 .query_map([], |row| {
707 let name: String = row.get(1)?;
708 let unique: bool = row.get::<_, i32>(2)? != 0;
709 Ok((name, unique))
710 })
711 .map_err(sqlite_err)?
712 .collect::<Result<Vec<_>, _>>()
713 .map_err(sqlite_err)?;
714
715 let ordered: BTreeMap<String, bool> = index_meta.into_iter().collect();
717
718 let mut indexes = Vec::new();
719 for (name, unique) in &ordered {
720 if name.starts_with("sqlite_autoindex_") {
722 continue;
723 }
724
725 let mut col_stmt = self
726 .conn
727 .prepare(&format!("PRAGMA index_info({})", quote_ident(name)))
728 .map_err(sqlite_err)?;
729
730 let columns: Vec<String> = col_stmt
731 .query_map([], |row| row.get(2))
732 .map_err(sqlite_err)?
733 .collect::<Result<Vec<String>, _>>()
734 .map_err(sqlite_err)?;
735
736 indexes.push(IndexSnapshot {
737 name: name.clone(),
738 columns,
739 unique: *unique,
740 });
741 }
742
743 Ok(indexes)
744 }
745}
746
747fn sqlite_err(e: rusqlite::Error) -> StorageError {
748 StorageError {
749 code: "SQLITE_QUERY_FAILED".into(),
750 message: format!("SQLite query failed: {e}"),
751 }
752}
753
754#[cfg(test)]
759mod tests {
760 use super::*;
761 use pylon_kernel::*;
762
763 fn test_manifest() -> AppManifest {
764 AppManifest {
765 manifest_version: MANIFEST_VERSION,
766 name: "test".into(),
767 version: "0.1.0".into(),
768 entities: vec![ManifestEntity {
769 name: "User".into(),
770 fields: vec![
771 ManifestField {
772 name: "email".into(),
773 field_type: "string".into(),
774 optional: false,
775 unique: true,
776 crdt: None,
777 },
778 ManifestField {
779 name: "displayName".into(),
780 field_type: "string".into(),
781 optional: false,
782 unique: false,
783 crdt: None,
784 },
785 ManifestField {
786 name: "age".into(),
787 field_type: "int".into(),
788 optional: true,
789 unique: false,
790 crdt: None,
791 },
792 ],
793 indexes: vec![ManifestIndex {
794 name: "by_email".into(),
795 fields: vec!["email".into()],
796 unique: true,
797 }],
798 relations: vec![],
799 search: None,
800 crdt: true,
801 }],
802 routes: vec![],
803 queries: vec![],
804 actions: vec![],
805 policies: vec![],
806 }
807 }
808
809 #[test]
810 fn create_table_sql_basic() {
811 let fields = vec![
812 FieldSpec {
813 name: "email".into(),
814 field_type: "string".into(),
815 optional: false,
816 unique: true,
817 },
818 FieldSpec {
819 name: "age".into(),
820 field_type: "int".into(),
821 optional: true,
822 unique: false,
823 },
824 ];
825 let sql = create_table_sql("User", &fields);
826 assert_eq!(
827 sql,
828 "CREATE TABLE IF NOT EXISTS \"User\" (id TEXT PRIMARY KEY NOT NULL, \"email\" TEXT NOT NULL UNIQUE, \"age\" INTEGER)"
829 );
830 }
831
832 #[test]
833 fn create_index_sql_basic() {
834 let sql = create_index_sql("User", "by_email", &["email".into()], true);
835 assert_eq!(
836 sql,
837 "CREATE UNIQUE INDEX IF NOT EXISTS \"User_by_email\" ON \"User\" (\"email\")"
838 );
839 }
840
841 #[test]
842 fn create_index_sql_non_unique() {
843 let sql = create_index_sql("Todo", "by_user", &["userId".into()], false);
844 assert_eq!(
845 sql,
846 "CREATE INDEX IF NOT EXISTS \"Todo_by_user\" ON \"Todo\" (\"userId\")"
847 );
848 }
849
850 #[test]
851 fn add_column_sql_basic() {
852 let field = FieldSpec {
853 name: "bio".into(),
854 field_type: "string".into(),
855 optional: true,
856 unique: false,
857 };
858 let sql = add_column_sql("User", &field);
859 assert_eq!(sql, "ALTER TABLE \"User\" ADD COLUMN \"bio\" TEXT");
860 }
861
862 #[test]
863 fn quote_ident_escapes_double_quotes() {
864 assert_eq!(quote_ident("normal"), "\"normal\"");
865 assert_eq!(quote_ident("has\"quote"), "\"has\"\"quote\"");
866 assert_eq!(
867 quote_ident("Robert'); DROP TABLE Students;--"),
868 "\"Robert'); DROP TABLE Students;--\""
869 );
870 }
871
872 #[test]
873 fn sqlite_adapter_creates_table() {
874 let adapter = SqliteAdapter::in_memory().unwrap();
875 let manifest = test_manifest();
876 let plan = adapter.plan_schema(&manifest).unwrap();
877 adapter.apply_schema(&plan).unwrap();
878
879 let table_count: i64 = adapter
881 .conn
882 .query_row(
883 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='User'",
884 [],
885 |row| row.get(0),
886 )
887 .unwrap();
888 assert_eq!(table_count, 1);
889 }
890
891 #[test]
892 fn sqlite_adapter_creates_index() {
893 let adapter = SqliteAdapter::in_memory().unwrap();
894 let manifest = test_manifest();
895 let plan = adapter.plan_schema(&manifest).unwrap();
896 adapter.apply_schema(&plan).unwrap();
897
898 let index_count: i64 = adapter
900 .conn
901 .query_row(
902 "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='User_by_email'",
903 [],
904 |row| row.get(0),
905 )
906 .unwrap();
907 assert_eq!(index_count, 1);
908 }
909
910 #[test]
911 fn sqlite_adapter_add_field() {
912 let adapter = SqliteAdapter::in_memory().unwrap();
913
914 let manifest = test_manifest();
916 let plan = adapter.plan_schema(&manifest).unwrap();
917 adapter.apply_schema(&plan).unwrap();
918
919 let add_plan = SchemaPlan {
921 operations: vec![SchemaOperation::AddField {
922 entity: "User".into(),
923 field: FieldSpec {
924 name: "bio".into(),
925 field_type: "string".into(),
926 optional: true,
927 unique: false,
928 },
929 }],
930 };
931 adapter.apply_schema(&add_plan).unwrap();
932
933 let has_bio: bool = adapter
935 .conn
936 .prepare("PRAGMA table_info(\"User\")")
937 .unwrap()
938 .query_map([], |row| {
939 let name: String = row.get(1)?;
940 Ok(name)
941 })
942 .unwrap()
943 .any(|r| r.unwrap() == "bio");
944 assert!(has_bio);
945 }
946
947 #[test]
948 fn sqlite_adapter_rejects_remove_entity() {
949 let adapter = SqliteAdapter::in_memory().unwrap();
950 let plan = SchemaPlan {
951 operations: vec![SchemaOperation::RemoveEntity {
952 name: "User".into(),
953 }],
954 };
955 let result = adapter.apply_schema(&plan);
956 assert!(result.is_err());
957 assert_eq!(result.unwrap_err().code, "SQLITE_OP_UNSUPPORTED");
958 }
959
960 #[test]
961 fn sqlite_adapter_rejects_remove_field() {
962 let adapter = SqliteAdapter::in_memory().unwrap();
963 let plan = SchemaPlan {
964 operations: vec![SchemaOperation::RemoveField {
965 entity: "User".into(),
966 field_name: "email".into(),
967 }],
968 };
969 let result = adapter.apply_schema(&plan);
970 assert!(result.is_err());
971 assert_eq!(result.unwrap_err().code, "SQLITE_OP_UNSUPPORTED");
972 }
973
974 #[test]
975 fn sqlite_adapter_column_types() {
976 assert_eq!(sqlite_column_type("string"), "TEXT");
977 assert_eq!(sqlite_column_type("int"), "INTEGER");
978 assert_eq!(sqlite_column_type("float"), "REAL");
979 assert_eq!(sqlite_column_type("bool"), "INTEGER");
980 assert_eq!(sqlite_column_type("datetime"), "TEXT");
981 assert_eq!(sqlite_column_type("richtext"), "TEXT");
982 assert_eq!(sqlite_column_type("id(User)"), "TEXT");
983 }
984
985 #[test]
988 fn introspect_empty_db() {
989 let adapter = SqliteAdapter::in_memory().unwrap();
990 let snapshot = adapter.read_schema().unwrap();
991 assert!(snapshot.tables.is_empty());
992 }
993
994 #[test]
995 fn introspect_after_apply() {
996 let adapter = SqliteAdapter::in_memory().unwrap();
997 let manifest = test_manifest();
998 let plan = adapter.plan_schema(&manifest).unwrap();
999 adapter.apply_schema(&plan).unwrap();
1000
1001 let snapshot = adapter.read_schema().unwrap();
1002
1003 assert_eq!(snapshot.tables.len(), 1);
1005 let user = &snapshot.tables[0];
1006 assert_eq!(user.name, "User");
1007
1008 assert_eq!(user.columns.len(), 4);
1010 assert_eq!(user.columns[0].name, "id");
1011 assert!(user.columns[0].primary_key);
1012 assert_eq!(user.columns[1].name, "email");
1013 assert_eq!(user.columns[1].column_type, "TEXT");
1014 assert!(user.columns[1].notnull);
1015 assert_eq!(user.columns[2].name, "displayName");
1016 assert_eq!(user.columns[3].name, "age");
1017 assert!(!user.columns[3].notnull); assert_eq!(user.indexes.len(), 1);
1021 assert_eq!(user.indexes[0].name, "User_by_email");
1022 assert_eq!(user.indexes[0].columns, vec!["email"]);
1023 assert!(user.indexes[0].unique);
1024 }
1025
1026 #[test]
1027 fn introspect_multiple_tables() {
1028 let adapter = SqliteAdapter::in_memory().unwrap();
1029
1030 let manifest = AppManifest {
1031 manifest_version: MANIFEST_VERSION,
1032 name: "test".into(),
1033 version: "0.1.0".into(),
1034 entities: vec![
1035 ManifestEntity {
1036 name: "Post".into(),
1037 fields: vec![ManifestField {
1038 name: "title".into(),
1039 field_type: "string".into(),
1040 optional: false,
1041 unique: false,
1042 crdt: None,
1043 }],
1044 indexes: vec![],
1045 relations: vec![],
1046 search: None,
1047 crdt: true,
1048 },
1049 ManifestEntity {
1050 name: "User".into(),
1051 fields: vec![ManifestField {
1052 name: "email".into(),
1053 field_type: "string".into(),
1054 optional: false,
1055 unique: true,
1056 crdt: None,
1057 }],
1058 indexes: vec![],
1059 relations: vec![],
1060 search: None,
1061 crdt: true,
1062 },
1063 ],
1064 routes: vec![],
1065 queries: vec![],
1066 actions: vec![],
1067 policies: vec![],
1068 };
1069
1070 let plan = adapter.plan_schema(&manifest).unwrap();
1071 adapter.apply_schema(&plan).unwrap();
1072
1073 let snapshot = adapter.read_schema().unwrap();
1074
1075 assert_eq!(snapshot.tables.len(), 2);
1077 assert_eq!(snapshot.tables[0].name, "Post");
1078 assert_eq!(snapshot.tables[1].name, "User");
1079 }
1080
1081 #[test]
1082 fn introspect_after_add_field() {
1083 let adapter = SqliteAdapter::in_memory().unwrap();
1084 let manifest = test_manifest();
1085 let plan = adapter.plan_schema(&manifest).unwrap();
1086 adapter.apply_schema(&plan).unwrap();
1087
1088 let add_plan = SchemaPlan {
1090 operations: vec![SchemaOperation::AddField {
1091 entity: "User".into(),
1092 field: FieldSpec {
1093 name: "bio".into(),
1094 field_type: "string".into(),
1095 optional: true,
1096 unique: false,
1097 },
1098 }],
1099 };
1100 adapter.apply_schema(&add_plan).unwrap();
1101
1102 let snapshot = adapter.read_schema().unwrap();
1103 let user = &snapshot.tables[0];
1104
1105 assert_eq!(user.columns.len(), 5);
1107 assert!(user.columns.iter().any(|c| c.name == "bio"));
1108 }
1109
1110 #[test]
1111 fn introspect_snapshot_is_deterministic() {
1112 let adapter = SqliteAdapter::in_memory().unwrap();
1113 let manifest = test_manifest();
1114 let plan = adapter.plan_schema(&manifest).unwrap();
1115 adapter.apply_schema(&plan).unwrap();
1116
1117 let s1 = adapter.read_schema().unwrap();
1118 let s2 = adapter.read_schema().unwrap();
1119 assert_eq!(s1, s2);
1120 }
1121
1122 #[test]
1125 fn plan_from_empty_db_creates_everything() {
1126 let adapter = SqliteAdapter::in_memory().unwrap();
1127 let manifest = test_manifest();
1128
1129 let plan = adapter.plan_from_live(&manifest).unwrap();
1130
1131 assert!(plan.operations.iter().any(|op| matches!(
1133 op,
1134 SchemaOperation::CreateEntity { name, .. } if name == "User"
1135 )));
1136 assert!(plan.operations.iter().any(|op| matches!(
1137 op,
1138 SchemaOperation::AddIndex { entity, name, .. } if entity == "User" && name == "by_email"
1139 )));
1140 }
1141
1142 #[test]
1143 fn plan_from_fully_applied_db_is_noop() {
1144 let adapter = SqliteAdapter::in_memory().unwrap();
1145 let manifest = test_manifest();
1146
1147 let initial = adapter.plan_from_live(&manifest).unwrap();
1149 adapter.apply_schema(&initial).unwrap();
1150
1151 let plan = adapter.plan_from_live(&manifest).unwrap();
1153 assert!(plan.is_empty(), "expected noop, got: {:?}", plan.operations);
1154 }
1155
1156 #[test]
1157 fn plan_detects_missing_column() {
1158 let adapter = SqliteAdapter::in_memory().unwrap();
1159
1160 adapter
1162 .conn
1163 .execute(
1164 "CREATE TABLE \"User\" (id TEXT PRIMARY KEY NOT NULL, email TEXT NOT NULL UNIQUE)",
1165 [],
1166 )
1167 .unwrap();
1168
1169 let manifest = test_manifest();
1170 let plan = adapter.plan_from_live(&manifest).unwrap();
1171
1172 let add_fields: Vec<_> = plan
1174 .operations
1175 .iter()
1176 .filter(|op| matches!(op, SchemaOperation::AddField { .. }))
1177 .collect();
1178 assert_eq!(add_fields.len(), 2);
1179 }
1180
1181 #[test]
1182 fn plan_detects_missing_index() {
1183 let adapter = SqliteAdapter::in_memory().unwrap();
1184
1185 adapter
1187 .conn
1188 .execute(
1189 "CREATE TABLE \"User\" (id TEXT PRIMARY KEY NOT NULL, email TEXT NOT NULL UNIQUE, \"displayName\" TEXT NOT NULL, age INTEGER)",
1190 [],
1191 )
1192 .unwrap();
1193
1194 let manifest = test_manifest();
1195 let plan = adapter.plan_from_live(&manifest).unwrap();
1196
1197 assert!(plan.operations.iter().any(|op| matches!(
1199 op,
1200 SchemaOperation::AddIndex { entity, name, .. } if entity == "User" && name == "by_email"
1201 )));
1202 assert!(!plan
1203 .operations
1204 .iter()
1205 .any(|op| matches!(op, SchemaOperation::CreateEntity { .. })));
1206 }
1207
1208 fn push_meta(baseline: &str) -> PushMetadata<'_> {
1211 PushMetadata {
1212 manifest_version: 1,
1213 app_version: "0.1.0",
1214 baseline,
1215 }
1216 }
1217
1218 fn history_count(adapter: &SqliteAdapter) -> i64 {
1219 adapter
1220 .conn
1221 .query_row(
1222 &format!("SELECT COUNT(*) FROM {}", quote_ident(HISTORY_TABLE)),
1223 [],
1224 |row| row.get(0),
1225 )
1226 .unwrap()
1227 }
1228
1229 #[test]
1230 fn history_table_created_on_apply() {
1231 let adapter = SqliteAdapter::in_memory().unwrap();
1232 let manifest = test_manifest();
1233 let plan = adapter.plan_from_live(&manifest).unwrap();
1234 adapter
1235 .apply_with_history(&plan, &push_meta("live_sqlite"))
1236 .unwrap();
1237
1238 let table_exists: i64 = adapter
1239 .conn
1240 .query_row(
1241 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
1242 [HISTORY_TABLE],
1243 |row| row.get(0),
1244 )
1245 .unwrap();
1246 assert_eq!(table_exists, 1);
1247 }
1248
1249 #[test]
1250 fn history_row_inserted_on_apply() {
1251 let adapter = SqliteAdapter::in_memory().unwrap();
1252 let manifest = test_manifest();
1253 let plan = adapter.plan_from_live(&manifest).unwrap();
1254 adapter
1255 .apply_with_history(&plan, &push_meta("live_sqlite"))
1256 .unwrap();
1257
1258 assert_eq!(history_count(&adapter), 1);
1259
1260 let (mv, av, baseline, op_count): (i64, String, String, i64) = adapter
1262 .conn
1263 .query_row(
1264 &format!(
1265 "SELECT manifest_version, app_version, baseline, operation_count FROM {} LIMIT 1",
1266 quote_ident(HISTORY_TABLE)
1267 ),
1268 [],
1269 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
1270 )
1271 .unwrap();
1272 assert_eq!(mv, 1);
1273 assert_eq!(av, "0.1.0");
1274 assert_eq!(baseline, "live_sqlite");
1275 assert_eq!(op_count, 2); }
1277
1278 #[test]
1279 fn noop_push_also_recorded() {
1280 let adapter = SqliteAdapter::in_memory().unwrap();
1281 let manifest = test_manifest();
1282
1283 let plan1 = adapter.plan_from_live(&manifest).unwrap();
1285 adapter
1286 .apply_with_history(&plan1, &push_meta("live_sqlite"))
1287 .unwrap();
1288
1289 let plan2 = adapter.plan_from_live(&manifest).unwrap();
1291 assert!(plan2.is_empty());
1292 adapter
1293 .apply_with_history(&plan2, &push_meta("live_sqlite"))
1294 .unwrap();
1295
1296 assert_eq!(history_count(&adapter), 2);
1298
1299 let op_count: i64 = adapter
1301 .conn
1302 .query_row(
1303 &format!(
1304 "SELECT operation_count FROM {} ORDER BY id DESC LIMIT 1",
1305 quote_ident(HISTORY_TABLE)
1306 ),
1307 [],
1308 |row| row.get(0),
1309 )
1310 .unwrap();
1311 assert_eq!(op_count, 0);
1312 }
1313
1314 #[test]
1315 fn history_plan_json_is_valid() {
1316 let adapter = SqliteAdapter::in_memory().unwrap();
1317 let manifest = test_manifest();
1318 let plan = adapter.plan_from_live(&manifest).unwrap();
1319 adapter
1320 .apply_with_history(&plan, &push_meta("live_sqlite"))
1321 .unwrap();
1322
1323 let plan_json: String = adapter
1324 .conn
1325 .query_row(
1326 &format!(
1327 "SELECT plan_json FROM {} LIMIT 1",
1328 quote_ident(HISTORY_TABLE)
1329 ),
1330 [],
1331 |row| row.get(0),
1332 )
1333 .unwrap();
1334
1335 let parsed: serde_json::Value = serde_json::from_str(&plan_json).unwrap();
1337 assert!(parsed.get("operations").unwrap().is_array());
1338 }
1339
1340 #[test]
1341 fn history_table_excluded_from_introspection() {
1342 let adapter = SqliteAdapter::in_memory().unwrap();
1343 let manifest = test_manifest();
1344 let plan = adapter.plan_from_live(&manifest).unwrap();
1345 adapter
1346 .apply_with_history(&plan, &push_meta("live_sqlite"))
1347 .unwrap();
1348
1349 let snapshot = adapter.read_schema().unwrap();
1350 assert!(!snapshot.tables.iter().any(|t| t.name.starts_with("_pylon")));
1351 }
1352
1353 #[test]
1356 fn read_history_empty_db() {
1357 let adapter = SqliteAdapter::in_memory().unwrap();
1358 let entries = adapter.read_history(None).unwrap();
1359 assert!(entries.is_empty());
1360 }
1361
1362 #[test]
1363 fn read_history_after_one_push() {
1364 let adapter = SqliteAdapter::in_memory().unwrap();
1365 let manifest = test_manifest();
1366 let plan = adapter.plan_from_live(&manifest).unwrap();
1367 adapter
1368 .apply_with_history(&plan, &push_meta("live_sqlite"))
1369 .unwrap();
1370
1371 let entries = adapter.read_history(None).unwrap();
1372 assert_eq!(entries.len(), 1);
1373 assert_eq!(entries[0].manifest_version, 1);
1374 assert_eq!(entries[0].app_version, "0.1.0");
1375 assert_eq!(entries[0].baseline, "live_sqlite");
1376 assert_eq!(entries[0].operation_count, 2); }
1378
1379 #[test]
1380 fn read_history_after_noop_push() {
1381 let adapter = SqliteAdapter::in_memory().unwrap();
1382 let manifest = test_manifest();
1383
1384 let plan1 = adapter.plan_from_live(&manifest).unwrap();
1385 adapter
1386 .apply_with_history(&plan1, &push_meta("live_sqlite"))
1387 .unwrap();
1388
1389 let plan2 = adapter.plan_from_live(&manifest).unwrap();
1390 adapter
1391 .apply_with_history(&plan2, &push_meta("live_sqlite"))
1392 .unwrap();
1393
1394 let entries = adapter.read_history(None).unwrap();
1395 assert_eq!(entries.len(), 2);
1396 assert_eq!(entries[0].operation_count, 0);
1398 assert_eq!(entries[1].operation_count, 2);
1399 }
1400
1401 #[test]
1402 fn read_history_newest_first() {
1403 let adapter = SqliteAdapter::in_memory().unwrap();
1404 let manifest = test_manifest();
1405
1406 let plan = adapter.plan_from_live(&manifest).unwrap();
1407 adapter
1408 .apply_with_history(
1409 &plan,
1410 &PushMetadata {
1411 manifest_version: 1,
1412 app_version: "0.1.0",
1413 baseline: "first",
1414 },
1415 )
1416 .unwrap();
1417
1418 let plan2 = adapter.plan_from_live(&manifest).unwrap();
1420 adapter
1421 .apply_with_history(
1422 &plan2,
1423 &PushMetadata {
1424 manifest_version: 1,
1425 app_version: "0.2.0",
1426 baseline: "second",
1427 },
1428 )
1429 .unwrap();
1430
1431 let entries = adapter.read_history(None).unwrap();
1432 assert_eq!(entries.len(), 2);
1433 assert_eq!(entries[0].baseline, "second");
1434 assert_eq!(entries[1].baseline, "first");
1435 }
1436
1437 #[test]
1438 fn read_history_with_limit() {
1439 let adapter = SqliteAdapter::in_memory().unwrap();
1440 let manifest = test_manifest();
1441
1442 let plan1 = adapter.plan_from_live(&manifest).unwrap();
1444 adapter
1445 .apply_with_history(&plan1, &push_meta("live_sqlite"))
1446 .unwrap();
1447 let plan2 = adapter.plan_from_live(&manifest).unwrap();
1448 adapter
1449 .apply_with_history(&plan2, &push_meta("live_sqlite"))
1450 .unwrap();
1451
1452 let all = adapter.read_history(None).unwrap();
1453 assert_eq!(all.len(), 2);
1454
1455 let limited = adapter.read_history(Some(1)).unwrap();
1456 assert_eq!(limited.len(), 1);
1457 }
1458
1459 #[test]
1460 fn read_history_entry_by_id() {
1461 let adapter = SqliteAdapter::in_memory().unwrap();
1462 let manifest = test_manifest();
1463
1464 let plan = adapter.plan_from_live(&manifest).unwrap();
1465 adapter
1466 .apply_with_history(&plan, &push_meta("live_sqlite"))
1467 .unwrap();
1468
1469 let entries = adapter.read_history(None).unwrap();
1470 let id = &entries[0].id;
1471
1472 let entry = adapter.read_history_entry(id).unwrap().unwrap();
1473 assert_eq!(&entry.id, id);
1474 assert_eq!(entry.operation_count, 2);
1475 }
1476
1477 #[test]
1478 fn read_history_entry_missing_id() {
1479 let adapter = SqliteAdapter::in_memory().unwrap();
1480 let result = adapter.read_history_entry("nonexistent").unwrap();
1481 assert!(result.is_none());
1482 }
1483
1484 #[test]
1485 fn history_entry_has_parsed_plan() {
1486 let adapter = SqliteAdapter::in_memory().unwrap();
1487 let manifest = test_manifest();
1488
1489 let plan = adapter.plan_from_live(&manifest).unwrap();
1490 adapter
1491 .apply_with_history(&plan, &push_meta("live_sqlite"))
1492 .unwrap();
1493
1494 let entries = adapter.read_history(None).unwrap();
1495 let entry = &entries[0];
1496
1497 assert!(entry.plan.is_some());
1499 let parsed_plan = entry.plan.as_ref().unwrap();
1500 assert!(!parsed_plan.operations.is_empty());
1501
1502 assert!(!entry.plan_json.is_empty());
1504 }
1505}