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.push(PathSegmentRef::Field(Cow::Borrowed(name)));
159 self
160 }
161
162 #[inline]
164 pub fn field_owned(mut self, name: String) -> Self {
165 self.segments.push(PathSegmentRef::Field(Cow::Owned(name)));
166 self
167 }
168
169 #[inline]
171 pub fn index(mut self, idx: i64) -> Self {
172 self.segments.push(PathSegmentRef::Index(idx));
173 self
174 }
175
176 #[inline]
178 pub fn all(mut self) -> Self {
179 self.segments.push(PathSegmentRef::Wildcard);
180 self
181 }
182
183 #[inline]
185 pub fn text(mut self) -> Self {
186 self.as_text = true;
187 self
188 }
189
190 pub fn is_zero_copy(&self) -> bool {
192 matches!(self.column, Cow::Borrowed(_))
193 && self.segments.iter().all(|s| match s {
194 PathSegmentRef::Field(cow) => matches!(cow, Cow::Borrowed(_)),
195 _ => true,
196 })
197 }
198
199 #[inline]
201 pub fn depth(&self) -> usize {
202 self.segments.len()
203 }
204
205 pub fn to_sql(&self, db_type: DatabaseType) -> String {
207 match db_type {
208 DatabaseType::PostgreSQL => self.to_postgres_sql(),
209 DatabaseType::MySQL => self.to_mysql_sql(),
210 DatabaseType::SQLite => self.to_sqlite_sql(),
211 DatabaseType::MSSQL => self.to_mssql_sql(),
212 }
213 }
214
215 fn to_postgres_sql(&self) -> String {
216 let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
217 sql.push_str(&self.column);
218
219 let last_idx = self.segments.len().saturating_sub(1);
220 for (i, segment) in self.segments.iter().enumerate() {
221 match segment {
222 PathSegmentRef::Field(name) => {
223 if self.as_text && i == last_idx {
224 sql.push_str(" ->> '");
225 } else {
226 sql.push_str(" -> '");
227 }
228 sql.push_str(name);
229 sql.push('\'');
230 }
231 PathSegmentRef::Index(idx) => {
232 if self.as_text && i == last_idx {
233 sql.push_str(" ->> ");
234 } else {
235 sql.push_str(" -> ");
236 }
237 sql.push_str(&idx.to_string());
238 }
239 PathSegmentRef::Wildcard => {
240 sql.push_str(" -> '*'");
241 }
242 PathSegmentRef::RecursiveDescent => {
243 sql.push_str(" #> '{}'");
245 }
246 }
247 }
248
249 sql
250 }
251
252 fn to_mysql_sql(&self) -> String {
253 let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
254 sql.push_str(&self.column);
255
256 let last_idx = self.segments.len().saturating_sub(1);
257 for (i, segment) in self.segments.iter().enumerate() {
258 match segment {
259 PathSegmentRef::Field(name) => {
260 if self.as_text && i == last_idx {
261 sql.push_str(" ->> '$.");
262 } else {
263 sql.push_str(" -> '$.");
264 }
265 sql.push_str(name);
266 sql.push('\'');
267 }
268 PathSegmentRef::Index(idx) => {
269 sql.push_str(" -> '$[");
270 sql.push_str(&idx.to_string());
271 sql.push_str("]'");
272 }
273 PathSegmentRef::Wildcard => {
274 sql.push_str(" -> '$[*]'");
275 }
276 PathSegmentRef::RecursiveDescent => {
277 sql.push_str(" -> '$**'");
278 }
279 }
280 }
281
282 sql
283 }
284
285 fn to_sqlite_sql(&self) -> String {
286 let mut path = String::from("$");
288 for segment in &self.segments {
289 match segment {
290 PathSegmentRef::Field(name) => {
291 path.push('.');
292 path.push_str(name);
293 }
294 PathSegmentRef::Index(idx) => {
295 path.push('[');
296 path.push_str(&idx.to_string());
297 path.push(']');
298 }
299 PathSegmentRef::Wildcard => {
300 path.push_str("[*]");
301 }
302 PathSegmentRef::RecursiveDescent => {
303 path.push_str("..");
304 }
305 }
306 }
307
308 format!("json_extract({}, '{}')", self.column, path)
309 }
310
311 fn to_mssql_sql(&self) -> String {
312 let mut path = String::from("$");
314 for segment in &self.segments {
315 match segment {
316 PathSegmentRef::Field(name) => {
317 path.push('.');
318 path.push_str(name);
319 }
320 PathSegmentRef::Index(idx) => {
321 path.push('[');
322 path.push_str(&idx.to_string());
323 path.push(']');
324 }
325 PathSegmentRef::Wildcard | PathSegmentRef::RecursiveDescent => {
326 path.push_str("[0]");
328 }
329 }
330 }
331
332 if self.as_text {
333 format!("JSON_VALUE({}, '{}')", self.column, path)
334 } else {
335 format!("JSON_QUERY({}, '{}')", self.column, path)
336 }
337 }
338
339 pub fn to_owned(&self) -> crate::json::JsonPath {
341 crate::json::JsonPath {
342 column: self.column.to_string(),
343 segments: self
344 .segments
345 .iter()
346 .map(|s| match s {
347 PathSegmentRef::Field(cow) => crate::json::PathSegment::Field(cow.to_string()),
348 PathSegmentRef::Index(i) => crate::json::PathSegment::Index(*i),
349 PathSegmentRef::Wildcard => crate::json::PathSegment::Wildcard,
350 PathSegmentRef::RecursiveDescent => crate::json::PathSegment::RecursiveDescent,
351 })
352 .collect(),
353 as_text: self.as_text,
354 }
355 }
356}
357
358impl<'a> From<&'a crate::json::JsonPath> for JsonPathRef<'a> {
359 fn from(path: &'a crate::json::JsonPath) -> Self {
360 Self {
361 column: Cow::Borrowed(&path.column),
362 segments: path
363 .segments
364 .iter()
365 .map(|s| match s {
366 crate::json::PathSegment::Field(name) => {
367 PathSegmentRef::Field(Cow::Borrowed(name))
368 }
369 crate::json::PathSegment::Index(i) => PathSegmentRef::Index(*i),
370 crate::json::PathSegment::Wildcard => PathSegmentRef::Wildcard,
371 crate::json::PathSegment::RecursiveDescent => PathSegmentRef::RecursiveDescent,
372 })
373 .collect(),
374 as_text: path.as_text,
375 }
376 }
377}
378
379#[derive(Debug, Clone, Default)]
402pub struct WindowSpecRef<'a> {
403 pub partition_by: SmallVec<[Cow<'a, str>; 4]>,
405 pub order_by: SmallVec<[(Cow<'a, str>, SortOrder); 4]>,
407 pub frame: Option<FrameRef<'a>>,
409 pub window_ref: Option<Cow<'a, str>>,
411}
412
413#[derive(Debug, Clone)]
415pub struct FrameRef<'a> {
416 pub frame_type: FrameTypeRef,
418 pub start: FrameBoundRef<'a>,
420 pub end: Option<FrameBoundRef<'a>>,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426pub enum FrameTypeRef {
427 Rows,
428 Range,
429 Groups,
430}
431
432#[derive(Debug, Clone, PartialEq)]
434pub enum FrameBoundRef<'a> {
435 UnboundedPreceding,
436 Preceding(u32),
437 CurrentRow,
438 Following(u32),
439 UnboundedFollowing,
440 Expr(Cow<'a, str>),
442}
443
444impl<'a> WindowSpecRef<'a> {
445 #[inline]
447 pub fn new() -> Self {
448 Self::default()
449 }
450
451 #[inline]
453 pub fn partition_by(mut self, columns: &[&'a str]) -> Self {
454 self.partition_by
455 .extend(columns.iter().map(|&s| Cow::Borrowed(s)));
456 self
457 }
458
459 #[inline]
461 pub fn partition_by_owned<I, S>(mut self, columns: I) -> Self
462 where
463 I: IntoIterator<Item = S>,
464 S: Into<String>,
465 {
466 self.partition_by
467 .extend(columns.into_iter().map(|s| Cow::Owned(s.into())));
468 self
469 }
470
471 #[inline]
473 pub fn partition_by_col(mut self, column: &'a str) -> Self {
474 self.partition_by.push(Cow::Borrowed(column));
475 self
476 }
477
478 #[inline]
480 pub fn order_by_asc(mut self, column: &'a str) -> Self {
481 self.order_by.push((Cow::Borrowed(column), SortOrder::Asc));
482 self
483 }
484
485 #[inline]
487 pub fn order_by_desc(mut self, column: &'a str) -> Self {
488 self.order_by
489 .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.iter().all(|c| matches!(c, Cow::Borrowed(_)))
548 && self
549 .order_by
550 .iter()
551 .all(|(c, _)| matches!(c, Cow::Borrowed(_)))
552 && self
553 .window_ref
554 .as_ref()
555 .map(|w| matches!(w, Cow::Borrowed(_)))
556 .unwrap_or(true)
557 }
558
559 pub fn to_sql(&self, _db_type: DatabaseType) -> String {
561 if let Some(ref name) = self.window_ref {
563 return format!("OVER {}", name);
564 }
565
566 let mut parts: SmallVec<[String; 4]> = SmallVec::new();
567
568 if !self.partition_by.is_empty() {
570 let cols: Vec<&str> = self.partition_by.iter().map(|c| c.as_ref()).collect();
571 parts.push(format!("PARTITION BY {}", cols.join(", ")));
572 }
573
574 if !self.order_by.is_empty() {
576 let cols: Vec<String> = self
577 .order_by
578 .iter()
579 .map(|(col, order)| {
580 format!(
581 "{} {}",
582 col,
583 match order {
584 SortOrder::Asc => "ASC",
585 SortOrder::Desc => "DESC",
586 }
587 )
588 })
589 .collect();
590 parts.push(format!("ORDER BY {}", cols.join(", ")));
591 }
592
593 if let Some(ref frame) = self.frame {
595 let frame_type = match frame.frame_type {
596 FrameTypeRef::Rows => "ROWS",
597 FrameTypeRef::Range => "RANGE",
598 FrameTypeRef::Groups => "GROUPS",
599 };
600
601 let start = frame_bound_to_sql(&frame.start);
602
603 if let Some(ref end) = frame.end {
604 let end_sql = frame_bound_to_sql(end);
605 parts.push(format!("{} BETWEEN {} AND {}", frame_type, start, end_sql));
606 } else {
607 parts.push(format!("{} {}", frame_type, start));
608 }
609 }
610
611 if parts.is_empty() {
612 "OVER ()".to_string()
613 } else {
614 format!("OVER ({})", parts.join(" "))
615 }
616 }
617}
618
619fn frame_bound_to_sql(bound: &FrameBoundRef<'_>) -> String {
620 match bound {
621 FrameBoundRef::UnboundedPreceding => "UNBOUNDED PRECEDING".to_string(),
622 FrameBoundRef::Preceding(n) => format!("{} PRECEDING", n),
623 FrameBoundRef::CurrentRow => "CURRENT ROW".to_string(),
624 FrameBoundRef::Following(n) => format!("{} FOLLOWING", n),
625 FrameBoundRef::UnboundedFollowing => "UNBOUNDED FOLLOWING".to_string(),
626 FrameBoundRef::Expr(expr) => expr.to_string(),
627 }
628}
629
630#[derive(Debug, Clone, Default)]
652pub struct CteRef<'a> {
653 pub name: Cow<'a, str>,
655 pub columns: SmallVec<[Cow<'a, str>; 8]>,
657 pub query: Cow<'a, str>,
659 pub recursive: bool,
661 pub materialized: Option<bool>,
663}
664
665impl<'a> CteRef<'a> {
666 #[inline]
668 pub fn new(name: &'a str) -> Self {
669 Self {
670 name: Cow::Borrowed(name),
671 columns: SmallVec::new(),
672 query: Cow::Borrowed(""),
673 recursive: false,
674 materialized: None,
675 }
676 }
677
678 #[inline]
680 pub fn owned(name: String) -> Self {
681 Self {
682 name: Cow::Owned(name),
683 columns: SmallVec::new(),
684 query: Cow::Borrowed(""),
685 recursive: false,
686 materialized: None,
687 }
688 }
689
690 #[inline]
692 pub fn columns(mut self, cols: &[&'a str]) -> Self {
693 self.columns.clear();
694 self.columns.extend(cols.iter().map(|&s| Cow::Borrowed(s)));
695 self
696 }
697
698 #[inline]
700 pub fn columns_owned<I, S>(mut self, cols: I) -> Self
701 where
702 I: IntoIterator<Item = S>,
703 S: Into<String>,
704 {
705 self.columns.clear();
706 self.columns
707 .extend(cols.into_iter().map(|s| Cow::Owned(s.into())));
708 self
709 }
710
711 #[inline]
713 pub fn column(mut self, col: &'a str) -> Self {
714 self.columns.push(Cow::Borrowed(col));
715 self
716 }
717
718 #[inline]
720 pub fn query(mut self, q: &'a str) -> Self {
721 self.query = Cow::Borrowed(q);
722 self
723 }
724
725 #[inline]
727 pub fn query_owned(mut self, q: String) -> Self {
728 self.query = Cow::Owned(q);
729 self
730 }
731
732 #[inline]
734 pub fn recursive(mut self) -> Self {
735 self.recursive = true;
736 self
737 }
738
739 #[inline]
741 pub fn materialized(mut self, mat: bool) -> Self {
742 self.materialized = Some(mat);
743 self
744 }
745
746 pub fn is_zero_copy(&self) -> bool {
748 matches!(self.name, Cow::Borrowed(_))
749 && matches!(self.query, Cow::Borrowed(_))
750 && self.columns.iter().all(|c| matches!(c, Cow::Borrowed(_)))
751 }
752
753 pub fn to_sql(&self, db_type: DatabaseType) -> String {
755 let mut sql = String::with_capacity(64 + self.query.len());
756
757 sql.push_str(&self.name);
758
759 if !self.columns.is_empty() {
761 sql.push_str(" (");
762 let cols: Vec<&str> = self.columns.iter().map(|c| c.as_ref()).collect();
763 sql.push_str(&cols.join(", "));
764 sql.push(')');
765 }
766
767 sql.push_str(" AS ");
768
769 if matches!(db_type, DatabaseType::PostgreSQL) {
771 if let Some(mat) = self.materialized {
772 if mat {
773 sql.push_str("MATERIALIZED ");
774 } else {
775 sql.push_str("NOT MATERIALIZED ");
776 }
777 }
778 }
779
780 sql.push('(');
781 sql.push_str(&self.query);
782 sql.push(')');
783
784 sql
785 }
786
787 pub fn to_owned_cte(&self) -> crate::cte::Cte {
789 crate::cte::Cte {
790 name: self.name.to_string(),
791 columns: self.columns.iter().map(|c| c.to_string()).collect(),
792 query: self.query.to_string(),
793 recursive: self.recursive,
794 materialized: self.materialized.map(|m| {
795 if m {
796 crate::cte::Materialized::Yes
797 } else {
798 crate::cte::Materialized::No
799 }
800 }),
801 search: None,
802 cycle: None,
803 }
804 }
805}
806
807#[derive(Debug, Clone, Default)]
830pub struct WithClauseRef<'a> {
831 pub ctes: SmallVec<[CteRef<'a>; 4]>,
833 pub recursive: bool,
835}
836
837impl<'a> WithClauseRef<'a> {
838 #[inline]
840 pub fn new() -> Self {
841 Self::default()
842 }
843
844 #[inline]
846 pub fn cte(mut self, cte: CteRef<'a>) -> Self {
847 if cte.recursive {
848 self.recursive = true;
849 }
850 self.ctes.push(cte);
851 self
852 }
853
854 #[inline]
856 pub fn recursive(mut self) -> Self {
857 self.recursive = true;
858 self
859 }
860
861 pub fn to_sql(&self, db_type: DatabaseType) -> String {
863 if self.ctes.is_empty() {
864 return String::new();
865 }
866
867 let mut sql = String::with_capacity(256);
868
869 sql.push_str("WITH ");
870 if self.recursive {
871 sql.push_str("RECURSIVE ");
872 }
873
874 let cte_sqls: Vec<String> = self.ctes.iter().map(|c| c.to_sql(db_type)).collect();
875 sql.push_str(&cte_sqls.join(", "));
876
877 sql
878 }
879
880 pub fn build_select(
882 &self,
883 columns: &[&str],
884 from: &str,
885 db_type: DatabaseType,
886 ) -> String {
887 let with_sql = self.to_sql(db_type);
888 let cols = if columns.is_empty() || columns == ["*"] {
889 "*".to_string()
890 } else {
891 columns.join(", ")
892 };
893
894 if with_sql.is_empty() {
895 format!("SELECT {} FROM {}", cols, from)
896 } else {
897 format!("{} SELECT {} FROM {}", with_sql, cols, from)
898 }
899 }
900}
901
902#[cfg(test)]
907mod tests {
908 use super::*;
909
910 #[test]
911 fn test_json_path_ref_zero_copy() {
912 let path = JsonPathRef::new("data").field("user").field("name");
913
914 assert!(path.is_zero_copy());
915 assert_eq!(path.depth(), 2);
916
917 let sql = path.to_sql(DatabaseType::PostgreSQL);
918 assert!(sql.contains("data"));
919 assert!(sql.contains("user"));
920 assert!(sql.contains("name"));
921 }
922
923 #[test]
924 fn test_json_path_ref_with_index() {
925 let path = JsonPathRef::new("items").field("products").index(0).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}
1029
1030
1031