1use smallvec::SmallVec;
42use std::borrow::Cow;
43
44use crate::sql::DatabaseType;
45use crate::types::SortOrder;
46
47#[derive(Debug, Clone)]
73pub struct JsonPathRef<'a> {
74 pub column: Cow<'a, str>,
76 pub segments: SmallVec<[PathSegmentRef<'a>; 8]>,
78 pub as_text: bool,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub enum PathSegmentRef<'a> {
85 Field(Cow<'a, str>),
87 Index(i64),
89 Wildcard,
91 RecursiveDescent,
93}
94
95impl<'a> JsonPathRef<'a> {
96 #[inline]
98 pub fn new(column: &'a str) -> Self {
99 Self {
100 column: Cow::Borrowed(column),
101 segments: SmallVec::new(),
102 as_text: false,
103 }
104 }
105
106 #[inline]
108 pub fn owned(column: String) -> Self {
109 Self {
110 column: Cow::Owned(column),
111 segments: SmallVec::new(),
112 as_text: false,
113 }
114 }
115
116 pub fn from_path(column: &'a str, path: &str) -> Self {
120 let mut json_path = Self::new(column);
121
122 let path = path.trim_start_matches('$').trim_start_matches('.');
123
124 for segment in path.split('.') {
125 if segment.is_empty() {
126 continue;
127 }
128
129 if let Some(bracket_pos) = segment.find('[') {
130 let field_name = &segment[..bracket_pos];
131 if !field_name.is_empty() {
132 json_path
133 .segments
134 .push(PathSegmentRef::Field(Cow::Owned(field_name.to_string())));
135 }
136
137 if let Some(end_pos) = segment.find(']') {
138 let idx_str = &segment[bracket_pos + 1..end_pos];
139 if idx_str == "*" {
140 json_path.segments.push(PathSegmentRef::Wildcard);
141 } else if let Ok(i) = idx_str.parse::<i64>() {
142 json_path.segments.push(PathSegmentRef::Index(i));
143 }
144 }
145 } else {
146 json_path
147 .segments
148 .push(PathSegmentRef::Field(Cow::Owned(segment.to_string())));
149 }
150 }
151
152 json_path
153 }
154
155 #[inline]
157 pub fn field(mut self, name: &'a str) -> Self {
158 self.segments
159 .push(PathSegmentRef::Field(Cow::Borrowed(name)));
160 self
161 }
162
163 #[inline]
165 pub fn field_owned(mut self, name: String) -> Self {
166 self.segments.push(PathSegmentRef::Field(Cow::Owned(name)));
167 self
168 }
169
170 #[inline]
172 pub fn index(mut self, idx: i64) -> Self {
173 self.segments.push(PathSegmentRef::Index(idx));
174 self
175 }
176
177 #[inline]
179 pub fn all(mut self) -> Self {
180 self.segments.push(PathSegmentRef::Wildcard);
181 self
182 }
183
184 #[inline]
186 pub fn text(mut self) -> Self {
187 self.as_text = true;
188 self
189 }
190
191 pub fn is_zero_copy(&self) -> bool {
193 matches!(self.column, Cow::Borrowed(_))
194 && self.segments.iter().all(|s| match s {
195 PathSegmentRef::Field(cow) => matches!(cow, Cow::Borrowed(_)),
196 _ => true,
197 })
198 }
199
200 #[inline]
202 pub fn depth(&self) -> usize {
203 self.segments.len()
204 }
205
206 pub fn to_sql(&self, db_type: DatabaseType) -> String {
208 match db_type {
209 DatabaseType::PostgreSQL => self.to_postgres_sql(),
210 DatabaseType::MySQL => self.to_mysql_sql(),
211 DatabaseType::SQLite => self.to_sqlite_sql(),
212 DatabaseType::MSSQL => self.to_mssql_sql(),
213 }
214 }
215
216 fn to_postgres_sql(&self) -> String {
217 let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
218 sql.push_str(&self.column);
219
220 let last_idx = self.segments.len().saturating_sub(1);
221 for (i, segment) in self.segments.iter().enumerate() {
222 match segment {
223 PathSegmentRef::Field(name) => {
224 if self.as_text && i == last_idx {
225 sql.push_str(" ->> '");
226 } else {
227 sql.push_str(" -> '");
228 }
229 sql.push_str(name);
230 sql.push('\'');
231 }
232 PathSegmentRef::Index(idx) => {
233 if self.as_text && i == last_idx {
234 sql.push_str(" ->> ");
235 } else {
236 sql.push_str(" -> ");
237 }
238 sql.push_str(&idx.to_string());
239 }
240 PathSegmentRef::Wildcard => {
241 sql.push_str(" -> '*'");
242 }
243 PathSegmentRef::RecursiveDescent => {
244 sql.push_str(" #> '{}'");
246 }
247 }
248 }
249
250 sql
251 }
252
253 fn to_mysql_sql(&self) -> String {
254 let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
255 sql.push_str(&self.column);
256
257 let last_idx = self.segments.len().saturating_sub(1);
258 for (i, segment) in self.segments.iter().enumerate() {
259 match segment {
260 PathSegmentRef::Field(name) => {
261 if self.as_text && i == last_idx {
262 sql.push_str(" ->> '$.");
263 } else {
264 sql.push_str(" -> '$.");
265 }
266 sql.push_str(name);
267 sql.push('\'');
268 }
269 PathSegmentRef::Index(idx) => {
270 sql.push_str(" -> '$[");
271 sql.push_str(&idx.to_string());
272 sql.push_str("]'");
273 }
274 PathSegmentRef::Wildcard => {
275 sql.push_str(" -> '$[*]'");
276 }
277 PathSegmentRef::RecursiveDescent => {
278 sql.push_str(" -> '$**'");
279 }
280 }
281 }
282
283 sql
284 }
285
286 fn to_sqlite_sql(&self) -> String {
287 let mut path = String::from("$");
289 for segment in &self.segments {
290 match segment {
291 PathSegmentRef::Field(name) => {
292 path.push('.');
293 path.push_str(name);
294 }
295 PathSegmentRef::Index(idx) => {
296 path.push('[');
297 path.push_str(&idx.to_string());
298 path.push(']');
299 }
300 PathSegmentRef::Wildcard => {
301 path.push_str("[*]");
302 }
303 PathSegmentRef::RecursiveDescent => {
304 path.push_str("..");
305 }
306 }
307 }
308
309 format!("json_extract({}, '{}')", self.column, path)
310 }
311
312 fn to_mssql_sql(&self) -> String {
313 let mut path = String::from("$");
315 for segment in &self.segments {
316 match segment {
317 PathSegmentRef::Field(name) => {
318 path.push('.');
319 path.push_str(name);
320 }
321 PathSegmentRef::Index(idx) => {
322 path.push('[');
323 path.push_str(&idx.to_string());
324 path.push(']');
325 }
326 PathSegmentRef::Wildcard | PathSegmentRef::RecursiveDescent => {
327 path.push_str("[0]");
329 }
330 }
331 }
332
333 if self.as_text {
334 format!("JSON_VALUE({}, '{}')", self.column, path)
335 } else {
336 format!("JSON_QUERY({}, '{}')", self.column, path)
337 }
338 }
339
340 pub fn to_owned(&self) -> crate::json::JsonPath {
342 crate::json::JsonPath {
343 column: self.column.to_string(),
344 segments: self
345 .segments
346 .iter()
347 .map(|s| match s {
348 PathSegmentRef::Field(cow) => crate::json::PathSegment::Field(cow.to_string()),
349 PathSegmentRef::Index(i) => crate::json::PathSegment::Index(*i),
350 PathSegmentRef::Wildcard => crate::json::PathSegment::Wildcard,
351 PathSegmentRef::RecursiveDescent => crate::json::PathSegment::RecursiveDescent,
352 })
353 .collect(),
354 as_text: self.as_text,
355 }
356 }
357}
358
359impl<'a> From<&'a crate::json::JsonPath> for JsonPathRef<'a> {
360 fn from(path: &'a crate::json::JsonPath) -> Self {
361 Self {
362 column: Cow::Borrowed(&path.column),
363 segments: path
364 .segments
365 .iter()
366 .map(|s| match s {
367 crate::json::PathSegment::Field(name) => {
368 PathSegmentRef::Field(Cow::Borrowed(name))
369 }
370 crate::json::PathSegment::Index(i) => PathSegmentRef::Index(*i),
371 crate::json::PathSegment::Wildcard => PathSegmentRef::Wildcard,
372 crate::json::PathSegment::RecursiveDescent => PathSegmentRef::RecursiveDescent,
373 })
374 .collect(),
375 as_text: path.as_text,
376 }
377 }
378}
379
380#[derive(Debug, Clone, Default)]
403pub struct WindowSpecRef<'a> {
404 pub partition_by: SmallVec<[Cow<'a, str>; 4]>,
406 pub order_by: SmallVec<[(Cow<'a, str>, SortOrder); 4]>,
408 pub frame: Option<FrameRef<'a>>,
410 pub window_ref: Option<Cow<'a, str>>,
412}
413
414#[derive(Debug, Clone)]
416pub struct FrameRef<'a> {
417 pub frame_type: FrameTypeRef,
419 pub start: FrameBoundRef<'a>,
421 pub end: Option<FrameBoundRef<'a>>,
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum FrameTypeRef {
428 Rows,
429 Range,
430 Groups,
431}
432
433#[derive(Debug, Clone, PartialEq)]
435pub enum FrameBoundRef<'a> {
436 UnboundedPreceding,
437 Preceding(u32),
438 CurrentRow,
439 Following(u32),
440 UnboundedFollowing,
441 Expr(Cow<'a, str>),
443}
444
445impl<'a> WindowSpecRef<'a> {
446 #[inline]
448 pub fn new() -> Self {
449 Self::default()
450 }
451
452 #[inline]
454 pub fn partition_by(mut self, columns: &[&'a str]) -> Self {
455 self.partition_by
456 .extend(columns.iter().map(|&s| Cow::Borrowed(s)));
457 self
458 }
459
460 #[inline]
462 pub fn partition_by_owned<I, S>(mut self, columns: I) -> Self
463 where
464 I: IntoIterator<Item = S>,
465 S: Into<String>,
466 {
467 self.partition_by
468 .extend(columns.into_iter().map(|s| Cow::Owned(s.into())));
469 self
470 }
471
472 #[inline]
474 pub fn partition_by_col(mut self, column: &'a str) -> Self {
475 self.partition_by.push(Cow::Borrowed(column));
476 self
477 }
478
479 #[inline]
481 pub fn order_by_asc(mut self, column: &'a str) -> Self {
482 self.order_by.push((Cow::Borrowed(column), SortOrder::Asc));
483 self
484 }
485
486 #[inline]
488 pub fn order_by_desc(mut self, column: &'a str) -> Self {
489 self.order_by.push((Cow::Borrowed(column), SortOrder::Desc));
490 self
491 }
492
493 #[inline]
495 pub fn order_by(mut self, column: &'a str, order: SortOrder) -> Self {
496 self.order_by.push((Cow::Borrowed(column), order));
497 self
498 }
499
500 #[inline]
502 pub fn order_by_owned(mut self, column: String, order: SortOrder) -> Self {
503 self.order_by.push((Cow::Owned(column), order));
504 self
505 }
506
507 #[inline]
509 pub fn rows(mut self, start: FrameBoundRef<'a>, end: Option<FrameBoundRef<'a>>) -> Self {
510 self.frame = Some(FrameRef {
511 frame_type: FrameTypeRef::Rows,
512 start,
513 end,
514 });
515 self
516 }
517
518 #[inline]
520 pub fn range(mut self, start: FrameBoundRef<'a>, end: Option<FrameBoundRef<'a>>) -> Self {
521 self.frame = Some(FrameRef {
522 frame_type: FrameTypeRef::Range,
523 start,
524 end,
525 });
526 self
527 }
528
529 #[inline]
531 pub fn rows_unbounded_preceding(self) -> Self {
532 self.rows(
533 FrameBoundRef::UnboundedPreceding,
534 Some(FrameBoundRef::CurrentRow),
535 )
536 }
537
538 #[inline]
540 pub fn window_name(mut self, name: &'a str) -> Self {
541 self.window_ref = Some(Cow::Borrowed(name));
542 self
543 }
544
545 pub fn is_zero_copy(&self) -> bool {
547 self.partition_by
548 .iter()
549 .all(|c| matches!(c, Cow::Borrowed(_)))
550 && self
551 .order_by
552 .iter()
553 .all(|(c, _)| matches!(c, Cow::Borrowed(_)))
554 && self
555 .window_ref
556 .as_ref()
557 .map(|w| matches!(w, Cow::Borrowed(_)))
558 .unwrap_or(true)
559 }
560
561 pub fn to_sql(&self, _db_type: DatabaseType) -> String {
563 if let Some(ref name) = self.window_ref {
565 return format!("OVER {}", name);
566 }
567
568 let mut parts: SmallVec<[String; 4]> = SmallVec::new();
569
570 if !self.partition_by.is_empty() {
572 let cols: Vec<&str> = self.partition_by.iter().map(|c| c.as_ref()).collect();
573 parts.push(format!("PARTITION BY {}", cols.join(", ")));
574 }
575
576 if !self.order_by.is_empty() {
578 let cols: Vec<String> = self
579 .order_by
580 .iter()
581 .map(|(col, order)| {
582 format!(
583 "{} {}",
584 col,
585 match order {
586 SortOrder::Asc => "ASC",
587 SortOrder::Desc => "DESC",
588 }
589 )
590 })
591 .collect();
592 parts.push(format!("ORDER BY {}", cols.join(", ")));
593 }
594
595 if let Some(ref frame) = self.frame {
597 let frame_type = match frame.frame_type {
598 FrameTypeRef::Rows => "ROWS",
599 FrameTypeRef::Range => "RANGE",
600 FrameTypeRef::Groups => "GROUPS",
601 };
602
603 let start = frame_bound_to_sql(&frame.start);
604
605 if let Some(ref end) = frame.end {
606 let end_sql = frame_bound_to_sql(end);
607 parts.push(format!("{} BETWEEN {} AND {}", frame_type, start, end_sql));
608 } else {
609 parts.push(format!("{} {}", frame_type, start));
610 }
611 }
612
613 if parts.is_empty() {
614 "OVER ()".to_string()
615 } else {
616 format!("OVER ({})", parts.join(" "))
617 }
618 }
619}
620
621fn frame_bound_to_sql(bound: &FrameBoundRef<'_>) -> String {
622 match bound {
623 FrameBoundRef::UnboundedPreceding => "UNBOUNDED PRECEDING".to_string(),
624 FrameBoundRef::Preceding(n) => format!("{} PRECEDING", n),
625 FrameBoundRef::CurrentRow => "CURRENT ROW".to_string(),
626 FrameBoundRef::Following(n) => format!("{} FOLLOWING", n),
627 FrameBoundRef::UnboundedFollowing => "UNBOUNDED FOLLOWING".to_string(),
628 FrameBoundRef::Expr(expr) => expr.to_string(),
629 }
630}
631
632#[derive(Debug, Clone, Default)]
654pub struct CteRef<'a> {
655 pub name: Cow<'a, str>,
657 pub columns: SmallVec<[Cow<'a, str>; 8]>,
659 pub query: Cow<'a, str>,
661 pub recursive: bool,
663 pub materialized: Option<bool>,
665}
666
667impl<'a> CteRef<'a> {
668 #[inline]
670 pub fn new(name: &'a str) -> Self {
671 Self {
672 name: Cow::Borrowed(name),
673 columns: SmallVec::new(),
674 query: Cow::Borrowed(""),
675 recursive: false,
676 materialized: None,
677 }
678 }
679
680 #[inline]
682 pub fn owned(name: String) -> Self {
683 Self {
684 name: Cow::Owned(name),
685 columns: SmallVec::new(),
686 query: Cow::Borrowed(""),
687 recursive: false,
688 materialized: None,
689 }
690 }
691
692 #[inline]
694 pub fn columns(mut self, cols: &[&'a str]) -> Self {
695 self.columns.clear();
696 self.columns.extend(cols.iter().map(|&s| Cow::Borrowed(s)));
697 self
698 }
699
700 #[inline]
702 pub fn columns_owned<I, S>(mut self, cols: I) -> Self
703 where
704 I: IntoIterator<Item = S>,
705 S: Into<String>,
706 {
707 self.columns.clear();
708 self.columns
709 .extend(cols.into_iter().map(|s| Cow::Owned(s.into())));
710 self
711 }
712
713 #[inline]
715 pub fn column(mut self, col: &'a str) -> Self {
716 self.columns.push(Cow::Borrowed(col));
717 self
718 }
719
720 #[inline]
722 pub fn query(mut self, q: &'a str) -> Self {
723 self.query = Cow::Borrowed(q);
724 self
725 }
726
727 #[inline]
729 pub fn query_owned(mut self, q: String) -> Self {
730 self.query = Cow::Owned(q);
731 self
732 }
733
734 #[inline]
736 pub fn recursive(mut self) -> Self {
737 self.recursive = true;
738 self
739 }
740
741 #[inline]
743 pub fn materialized(mut self, mat: bool) -> Self {
744 self.materialized = Some(mat);
745 self
746 }
747
748 pub fn is_zero_copy(&self) -> bool {
750 matches!(self.name, Cow::Borrowed(_))
751 && matches!(self.query, Cow::Borrowed(_))
752 && self.columns.iter().all(|c| matches!(c, Cow::Borrowed(_)))
753 }
754
755 pub fn to_sql(&self, db_type: DatabaseType) -> String {
757 let mut sql = String::with_capacity(64 + self.query.len());
758
759 sql.push_str(&self.name);
760
761 if !self.columns.is_empty() {
763 sql.push_str(" (");
764 let cols: Vec<&str> = self.columns.iter().map(|c| c.as_ref()).collect();
765 sql.push_str(&cols.join(", "));
766 sql.push(')');
767 }
768
769 sql.push_str(" AS ");
770
771 if matches!(db_type, DatabaseType::PostgreSQL) {
773 if let Some(mat) = self.materialized {
774 if mat {
775 sql.push_str("MATERIALIZED ");
776 } else {
777 sql.push_str("NOT MATERIALIZED ");
778 }
779 }
780 }
781
782 sql.push('(');
783 sql.push_str(&self.query);
784 sql.push(')');
785
786 sql
787 }
788
789 pub fn to_owned_cte(&self) -> crate::cte::Cte {
791 crate::cte::Cte {
792 name: self.name.to_string(),
793 columns: self.columns.iter().map(|c| c.to_string()).collect(),
794 query: self.query.to_string(),
795 recursive: self.recursive,
796 materialized: self.materialized.map(|m| {
797 if m {
798 crate::cte::Materialized::Yes
799 } else {
800 crate::cte::Materialized::No
801 }
802 }),
803 search: None,
804 cycle: None,
805 }
806 }
807}
808
809#[derive(Debug, Clone, Default)]
832pub struct WithClauseRef<'a> {
833 pub ctes: SmallVec<[CteRef<'a>; 4]>,
835 pub recursive: bool,
837}
838
839impl<'a> WithClauseRef<'a> {
840 #[inline]
842 pub fn new() -> Self {
843 Self::default()
844 }
845
846 #[inline]
848 pub fn cte(mut self, cte: CteRef<'a>) -> Self {
849 if cte.recursive {
850 self.recursive = true;
851 }
852 self.ctes.push(cte);
853 self
854 }
855
856 #[inline]
858 pub fn recursive(mut self) -> Self {
859 self.recursive = true;
860 self
861 }
862
863 pub fn to_sql(&self, db_type: DatabaseType) -> String {
865 if self.ctes.is_empty() {
866 return String::new();
867 }
868
869 let mut sql = String::with_capacity(256);
870
871 sql.push_str("WITH ");
872 if self.recursive {
873 sql.push_str("RECURSIVE ");
874 }
875
876 let cte_sqls: Vec<String> = self.ctes.iter().map(|c| c.to_sql(db_type)).collect();
877 sql.push_str(&cte_sqls.join(", "));
878
879 sql
880 }
881
882 pub fn build_select(&self, columns: &[&str], from: &str, db_type: DatabaseType) -> String {
884 let with_sql = self.to_sql(db_type);
885 let cols = if columns.is_empty() || columns == ["*"] {
886 "*".to_string()
887 } else {
888 columns.join(", ")
889 };
890
891 if with_sql.is_empty() {
892 format!("SELECT {} FROM {}", cols, from)
893 } else {
894 format!("{} SELECT {} FROM {}", with_sql, cols, from)
895 }
896 }
897}
898
899#[cfg(test)]
904mod tests {
905 use super::*;
906
907 #[test]
908 fn test_json_path_ref_zero_copy() {
909 let path = JsonPathRef::new("data").field("user").field("name");
910
911 assert!(path.is_zero_copy());
912 assert_eq!(path.depth(), 2);
913
914 let sql = path.to_sql(DatabaseType::PostgreSQL);
915 assert!(sql.contains("data"));
916 assert!(sql.contains("user"));
917 assert!(sql.contains("name"));
918 }
919
920 #[test]
921 fn test_json_path_ref_with_index() {
922 let path = JsonPathRef::new("items")
923 .field("products")
924 .index(0)
925 .field("name");
926
927 let sql = path.to_sql(DatabaseType::PostgreSQL);
928 assert!(sql.contains("products"));
929 assert!(sql.contains("0"));
930 assert!(sql.contains("name"));
931 }
932
933 #[test]
934 fn test_json_path_ref_as_text() {
935 let path = JsonPathRef::new("data").field("name").text();
936
937 let pg_sql = path.to_sql(DatabaseType::PostgreSQL);
938 assert!(pg_sql.contains("->>")); }
940
941 #[test]
942 fn test_window_spec_ref_zero_copy() {
943 let spec = WindowSpecRef::new()
944 .partition_by(&["dept", "team"])
945 .order_by_asc("salary");
946
947 assert!(spec.is_zero_copy());
948
949 let sql = spec.to_sql(DatabaseType::PostgreSQL);
950 assert!(sql.contains("PARTITION BY dept, team"));
951 assert!(sql.contains("ORDER BY salary ASC"));
952 }
953
954 #[test]
955 fn test_window_spec_ref_with_frame() {
956 let spec = WindowSpecRef::new()
957 .order_by_asc("date")
958 .rows_unbounded_preceding();
959
960 let sql = spec.to_sql(DatabaseType::PostgreSQL);
961 assert!(sql.contains("ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"));
962 }
963
964 #[test]
965 fn test_cte_ref_zero_copy() {
966 let cte = CteRef::new("active_users")
967 .columns(&["id", "name", "email"])
968 .query("SELECT id, name, email FROM users WHERE active = true");
969
970 assert!(cte.is_zero_copy());
971
972 let sql = cte.to_sql(DatabaseType::PostgreSQL);
973 assert!(sql.contains("active_users"));
974 assert!(sql.contains("id, name, email"));
975 assert!(sql.contains("SELECT id, name, email FROM users"));
976 }
977
978 #[test]
979 fn test_cte_ref_recursive() {
980 let cte = CteRef::new("tree")
981 .columns(&["id", "parent_id", "level"])
982 .query("SELECT id, parent_id, 1 FROM items WHERE parent_id IS NULL")
983 .recursive();
984
985 assert!(cte.recursive);
986 let sql = cte.to_sql(DatabaseType::PostgreSQL);
987 assert!(sql.contains("tree"));
988 }
989
990 #[test]
991 fn test_with_clause_ref() {
992 let with = WithClauseRef::new()
993 .cte(
994 CteRef::new("active_users")
995 .columns(&["id", "name"])
996 .query("SELECT id, name FROM users WHERE active = true"),
997 )
998 .cte(
999 CteRef::new("orders")
1000 .columns(&["user_id", "total"])
1001 .query("SELECT user_id, SUM(amount) FROM orders GROUP BY user_id"),
1002 );
1003
1004 let sql = with.build_select(&["*"], "active_users", DatabaseType::PostgreSQL);
1005 assert!(sql.contains("WITH"));
1006 assert!(sql.contains("active_users"));
1007 assert!(sql.contains("orders"));
1008 assert!(sql.contains("SELECT * FROM active_users"));
1009 }
1010
1011 #[test]
1012 fn test_json_path_ref_mysql() {
1013 let path = JsonPathRef::new("data").field("user").field("email");
1014
1015 let sql = path.to_sql(DatabaseType::MySQL);
1016 assert!(sql.contains("data"));
1017 assert!(sql.contains("$.user"));
1018 }
1019
1020 #[test]
1021 fn test_json_path_ref_sqlite() {
1022 let path = JsonPathRef::new("config").field("settings").field("theme");
1023
1024 let sql = path.to_sql(DatabaseType::SQLite);
1025 assert!(sql.contains("json_extract"));
1026 assert!(sql.contains("$.settings.theme"));
1027 }
1028}