jsql-neo 5.4.2 → 6.0.0-beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +198 -14
  2. package/index.d.ts +29 -0
  3. package/index.js +12 -2
  4. package/lib/ast.js +214 -0
  5. package/lib/database.js +26 -60
  6. package/lib/mongo_server.js +1 -1
  7. package/lib/native_client.js +43 -66
  8. package/lib/plugin.js +227 -3
  9. package/lib/plugins/audit.js +37 -0
  10. package/lib/plugins/index.js +72 -0
  11. package/lib/plugins/logger.js +35 -0
  12. package/lib/plugins/metrics.js +38 -0
  13. package/lib/plugins/timestamps.js +43 -0
  14. package/lib/plugins/validation.js +128 -0
  15. package/lib/sql.js +1212 -84
  16. package/lib/wasm_client.js +41 -57
  17. package/native/jsql-neo-native.node +0 -0
  18. package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
  19. package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
  20. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
  21. package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
  22. package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
  23. package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
  24. package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
  25. package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
  26. package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
  27. package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
  28. package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
  29. package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
  30. package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
  31. package/package.json +4 -3
  32. package/test/sql-parser.test.js +450 -0
  33. package/wasm/browser.mjs +12 -0
  34. package/wasm/browser_bg.mjs +269 -248
  35. package/wasm/jsql_neo_wasm.d.ts +6 -0
  36. package/wasm/jsql_neo_wasm.js +248 -127
  37. package/wasm/jsql_neo_wasm_bg.wasm +0 -0
  38. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
  39. package/wasm/package.json +1 -1
@@ -161,6 +161,32 @@ fn millis_to_string(ms: u64) -> String {
161
161
  }
162
162
  }
163
163
 
164
+ /// 行内时间戳格式化缓存:批量写入的行共享同一个毫秒时间戳,
165
+ /// 命中缓存可避免每行都做一次 chrono/JS Date 格式化与字符串分配。
166
+ #[derive(Default)]
167
+ pub struct TimeCache {
168
+ last_ms: u64,
169
+ last_str: String,
170
+ valid: bool,
171
+ }
172
+
173
+ impl TimeCache {
174
+ pub fn new() -> Self {
175
+ Self { last_ms: 0, last_str: String::new(), valid: false }
176
+ }
177
+
178
+ #[inline]
179
+ fn write(&mut self, out: &mut String, ms: u64) {
180
+ if !self.valid || self.last_ms != ms {
181
+ self.last_str.clear();
182
+ self.last_str.push_str(&millis_to_string(ms));
183
+ self.last_ms = ms;
184
+ self.valid = true;
185
+ }
186
+ out.push_str(&self.last_str);
187
+ }
188
+ }
189
+
164
190
  impl Table {
165
191
  pub fn new(name: String, schema: &[(String, FieldSchema)]) -> Self {
166
192
  let pk_field = schema.iter()
@@ -215,6 +241,29 @@ impl Table {
215
241
  }
216
242
  }
217
243
 
244
+ /// 运行期加列(供 `db.addColumn` 使用):扩展 schema / field_order / field_index,
245
+ /// 并为已有行按「default → 非空类型默认值 → null」的既有语义回填新列的值。
246
+ /// 返回 true 表示新增成功;false 表示该列已存在(no-op)。
247
+ pub fn add_column(&mut self, name: &str, fs: &FieldSchema) -> bool {
248
+ if self.field_index.contains_key(name) {
249
+ return false;
250
+ }
251
+ let fill = match &fs.default {
252
+ Some(dv) => FieldValue::from_json(dv.clone()),
253
+ None => if !fs.nullable { default_value(&fs.field_type) } else { FieldValue::Null },
254
+ };
255
+ let pos = self.field_order.len();
256
+ self.schema.insert(name.to_string(), fs.clone());
257
+ self.field_order.push(name.to_string());
258
+ self.field_index.insert(name.to_string(), pos);
259
+ for i in 0..self.rows.len() {
260
+ let start = self.rows[i].values_start + self.rows[i].num_values;
261
+ self.values.insert(start, fill.clone());
262
+ self.rows[i].num_values += 1;
263
+ }
264
+ true
265
+ }
266
+
218
267
  pub fn add_rows(&mut self, fields_batch: Vec<HashMap<String, serde_json::Value>>) -> Vec<u64> {
219
268
  let now = now_millis();
220
269
  let batch_len = fields_batch.len();
@@ -409,12 +458,32 @@ impl Table {
409
458
  }
410
459
 
411
460
  pub fn row_matches(&self, r: &RowStore, filter: &HashMap<String, serde_json::Value>) -> bool {
412
- let vals = self.get_row_values(r);
413
- filter.iter().all(|(k, v)| {
414
- self.field_index.get(k)
415
- .and_then(|&pos| vals.get(pos))
416
- .map_or(false, |fv| fv.eq_json(v))
417
- })
461
+ let conds = self.parse_filter(filter);
462
+ matches_filter(self.get_row_values(r), &conds)
463
+ }
464
+
465
+ /// 预解析过滤条件:字段名 → 行内下标 + 条件,避免逐行重复做 HashMap 查找。
466
+ /// 支持字段级操作符($eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$between/$exists/$like/$regex),
467
+ /// 一个字段多个操作符(如 {$gte:18,$lte:65})会展开成多条,需同时满足。
468
+ pub(crate) fn parse_filter<'a>(&self, filter: &'a HashMap<String, serde_json::Value>)
469
+ -> Vec<(usize, Cond<'a>)>
470
+ {
471
+ let mut out = Vec::with_capacity(filter.len());
472
+ for (k, v) in filter {
473
+ // 未知字段用哨兵下标,按「字段不存在」参与匹配(不再静默放行)
474
+ let pos = self.field_index.get(k).copied().unwrap_or(ABSENT_FIELD);
475
+ match v {
476
+ serde_json::Value::Object(map)
477
+ if !map.is_empty() && map.keys().all(|kk| kk.starts_with('$')) =>
478
+ {
479
+ for (op, target) in map {
480
+ out.push((pos, parse_op(op, target)));
481
+ }
482
+ }
483
+ other => out.push((pos, Cond::Eq(other))),
484
+ }
485
+ }
486
+ out
418
487
  }
419
488
 
420
489
  pub fn get_row(&self, id: u64) -> Option<Row> {
@@ -429,6 +498,7 @@ impl Table {
429
498
 
430
499
  pub fn get_rows_json(&self, ids: &[u64]) -> String {
431
500
  let mut out = String::with_capacity(ids.len() * 128);
501
+ let mut tc = TimeCache::new();
432
502
  out.push('[');
433
503
  let mut first = true;
434
504
  for &id in ids {
@@ -436,7 +506,7 @@ impl Table {
436
506
  if let Some(rs) = self.rows.get(idx) {
437
507
  if !first { out.push(','); }
438
508
  first = false;
439
- out.push_str(&self.to_row_json_string(rs));
509
+ self.write_row_json_into(rs, &mut out, &mut tc);
440
510
  }
441
511
  }
442
512
  }
@@ -458,30 +528,42 @@ impl Table {
458
528
  }
459
529
 
460
530
  fn to_row_json_string(&self, rs: &RowStore) -> String {
461
- let vals = self.get_row_values(rs);
462
531
  let mut out = String::with_capacity(160);
532
+ let mut tc = TimeCache::new();
533
+ self.write_row_json_into(rs, &mut out, &mut tc);
534
+ out
535
+ }
536
+
537
+ /// 直接把一行写成 JSON 追加到 `out`。
538
+ /// 相比先构造 `Row`(HashMap<String, Value>)再 serde 序列化,
539
+ /// 省掉了每个字段一次的字符串分配与一次完整的二次序列化。
540
+ pub fn write_row_json_into(&self, rs: &RowStore, out: &mut String, tc: &mut TimeCache) {
541
+ let vals = self.get_row_values(rs);
463
542
  out.push_str(r#"{"id":"#);
464
543
  out.push_str(itoa::Buffer::new().format(rs.id));
465
544
  out.push_str(r#","fields":{"#);
466
545
  for (i, name) in self.field_order.iter().enumerate() {
467
546
  if i > 0 { out.push(','); }
468
- write_json_string(&mut out, name);
547
+ write_json_string(out, name);
469
548
  out.push(':');
470
- if let Some(v) = vals.get(i) {
471
- v.write_json(&mut out);
472
- } else {
473
- out.push_str("null");
549
+ match vals.get(i) {
550
+ Some(v) => v.write_json(out),
551
+ None => out.push_str("null"),
474
552
  }
475
553
  }
476
- out.push_str(r#"},"created_at":"#);
477
- write_json_string(&mut out, &millis_to_string(rs.created_at));
478
- out.push_str(r#","updated_at":"#);
479
- write_json_string(&mut out, &millis_to_string(rs.updated_at));
480
- out.push_str("}");
481
- out
554
+ out.push_str(r#"},"created_at":""#);
555
+ tc.write(out, rs.created_at);
556
+ out.push_str(r#"","updated_at":""#);
557
+ tc.write(out, rs.updated_at);
558
+ out.push_str(r#""}"#);
482
559
  }
483
560
 
484
561
  pub fn update_row(&mut self, id: u64, data: HashMap<String, serde_json::Value>) -> bool {
562
+ self.update_row_ref(id, &data)
563
+ }
564
+
565
+ /// 借用版本:批量按 filter 更新时复用同一份 data,避免每行都克隆整个 HashMap。
566
+ pub fn update_row_ref(&mut self, id: u64, data: &HashMap<String, serde_json::Value>) -> bool {
485
567
  let idx = match self.pk_index.get(&id) {
486
568
  Some(i) => *i,
487
569
  None => return false,
@@ -492,9 +574,9 @@ impl Table {
492
574
  let nv = row.num_values;
493
575
  let s = row.values_start;
494
576
  for (k, v) in data {
495
- if let Some(&pos) = self.field_index.get(&k) {
577
+ if let Some(&pos) = self.field_index.get(k) {
496
578
  if pos < nv {
497
- self.values[s + pos] = FieldValue::from_json(v);
579
+ self.values[s + pos] = FieldValue::from_json(v.clone());
498
580
  }
499
581
  }
500
582
  }
@@ -651,38 +733,252 @@ impl Table {
651
733
  Ok(())
652
734
  }
653
735
 
736
+ /// 与 `find_rows` 语义一致,但直接把结果写成 JSON 字符串。
737
+ /// - 无排序时流式扫描,凑够 limit 立刻停止,不再先收集全表行指针
738
+ /// - 命中 limit 后仍多探一行,用于判断是否还有下一页(has_more)
739
+ /// 返回 (json, has_more)
740
+ pub fn find_rows_json(&self,
741
+ filter: &Option<HashMap<String, serde_json::Value>>,
742
+ limit: Option<usize>, offset: Option<usize>,
743
+ order_by: &Option<String>, order: &Option<String>) -> (String, bool) {
744
+ let o = offset.unwrap_or(0);
745
+ let cap = limit.unwrap_or(usize::MAX);
746
+ let conds = filter.as_ref().map(|f| self.parse_filter(f)).unwrap_or_default();
747
+
748
+ let mut out = String::with_capacity(128);
749
+ out.push('[');
750
+ let mut written = 0usize;
751
+ let mut has_more = false;
752
+ let mut tc = TimeCache::new();
753
+
754
+ let ordered = order_by.as_ref().and_then(|f| {
755
+ self.field_index.get(f).map(|&p| (p, order.as_deref() == Some("desc")))
756
+ });
757
+
758
+ match ordered {
759
+ Some((pos, desc)) => {
760
+ // 排序路径必须先收集全部匹配行
761
+ let mut matched: Vec<&RowStore> = self.rows.iter()
762
+ .filter(|r| matches_filter(self.get_row_values(r), &conds))
763
+ .collect();
764
+ matched.sort_by(|a, b| {
765
+ let cmp = cmp_field_values(
766
+ self.get_row_values(a).get(pos),
767
+ self.get_row_values(b).get(pos),
768
+ );
769
+ if desc { cmp.reverse() } else { cmp }
770
+ });
771
+ has_more = limit.is_some() && matched.len().saturating_sub(o) > cap;
772
+ for r in matched.into_iter().skip(o).take(cap) {
773
+ if written > 0 { out.push(','); }
774
+ self.write_row_json_into(r, &mut out, &mut tc);
775
+ written += 1;
776
+ }
777
+ }
778
+ None => {
779
+ let mut skipped = 0usize;
780
+ for r in &self.rows {
781
+ if !matches_filter(self.get_row_values(r), &conds) { continue; }
782
+ if skipped < o { skipped += 1; continue; }
783
+ if written >= cap { has_more = true; break; }
784
+ if written > 0 { out.push(','); }
785
+ self.write_row_json_into(r, &mut out, &mut tc);
786
+ written += 1;
787
+ }
788
+ }
789
+ }
790
+
791
+ out.push(']');
792
+ (out, has_more)
793
+ }
794
+
654
795
  pub fn find_rows(&self, filter: &Option<HashMap<String, serde_json::Value>>,
655
796
  limit: Option<usize>, offset: Option<usize>,
656
797
  order_by: &Option<String>, order: &Option<String>) -> Vec<Row> {
657
- let mut results: Vec<&RowStore> = self.rows.iter().collect();
798
+ let o = offset.unwrap_or(0);
799
+ let l = limit.unwrap_or(usize::MAX);
800
+ let conds = filter.as_ref().map(|f| self.parse_filter(f)).unwrap_or_default();
658
801
 
659
- if let Some(ref f) = filter {
660
- let filter_indices: Vec<(usize, &serde_json::Value)> = f.iter()
661
- .filter_map(|(k, v)| self.field_index.get(k).map(|&pos| (pos, v)))
802
+ if let Some(field) = order_by.as_ref().filter(|f| self.field_index.contains_key(*f)) {
803
+ let pos = self.field_index[field];
804
+ let desc = order.as_deref() == Some("desc");
805
+ let mut results: Vec<&RowStore> = self.rows.iter()
806
+ .filter(|r| matches_filter(self.get_row_values(r), &conds))
662
807
  .collect();
663
- results.retain(|r| {
664
- let vals = self.get_row_values(r);
665
- filter_indices.iter().all(|(pos, v)| {
666
- vals.get(*pos).map_or(false, |fv| fv.eq_json(v))
667
- })
808
+ results.sort_by(|a, b| {
809
+ let cmp = cmp_field_values(
810
+ self.get_row_values(a).get(pos),
811
+ self.get_row_values(b).get(pos),
812
+ );
813
+ if desc { cmp.reverse() } else { cmp }
668
814
  });
815
+ return results.into_iter().skip(o).take(l).map(|r| self.to_row(r)).collect();
669
816
  }
670
817
 
671
- if let Some(ref field) = order_by {
672
- let desc = order.as_deref() == Some("desc");
673
- if let Some(&pos) = self.field_index.get(field) {
674
- results.sort_by(|a, b| {
675
- let vals_a = self.get_row_values(a);
676
- let vals_b = self.get_row_values(b);
677
- let cmp = cmp_field_values(vals_a.get(pos), vals_b.get(pos));
678
- if desc { cmp.reverse() } else { cmp }
679
- });
818
+ // 无排序:流式扫描,凑够 limit 立刻停止,不先收集全表行指针
819
+ let mut out = Vec::with_capacity(l.min(self.rows.len()));
820
+ let mut skipped = 0usize;
821
+ for r in &self.rows {
822
+ if !matches_filter(self.get_row_values(r), &conds) { continue; }
823
+ if skipped < o { skipped += 1; continue; }
824
+ if out.len() >= l { break; }
825
+ out.push(self.to_row(r));
826
+ }
827
+ out
828
+ }
829
+ }
830
+
831
+ /// 过滤条件里出现 schema 之外的字段时用它作为哨兵下标:
832
+ /// `vals.get(SENTINEL)` 恒为 None,等价于「该字段不存在」,
833
+ /// 于是 {$exists:false}/{$ne:x} 等语义与 lib/table.js 的 `_matchRow` 一致,
834
+ /// 而 `{noSuchField: 5}` 会正确地匹配 0 行(旧实现会静默放行全部行)。
835
+ const ABSENT_FIELD: usize = usize::MAX;
836
+
837
+ /// 单字段查询条件(字段名已在解析阶段降为行内下标)
838
+ pub(crate) enum Cond<'a> {
839
+ Eq(&'a serde_json::Value),
840
+ Ne(&'a serde_json::Value),
841
+ Gt(&'a serde_json::Value),
842
+ Gte(&'a serde_json::Value),
843
+ Lt(&'a serde_json::Value),
844
+ Lte(&'a serde_json::Value),
845
+ In(&'a serde_json::Value),
846
+ Nin(&'a serde_json::Value),
847
+ /// 闭区间 [lo, hi]
848
+ Between(&'a serde_json::Value),
849
+ Exists(bool),
850
+ /// `$like`:SQL 风格通配(`%` = 任意多字符,`_` = 单字符),大小写不敏感。
851
+ /// 模式在解析阶段编译一次,逐行只做匹配。
852
+ Like(Box<regex_lite::Regex>),
853
+ /// `$regex`:JavaScript 风格正则,大小写敏感。
854
+ Regex(Box<regex_lite::Regex>),
855
+ /// 不认识的写法:永不匹配(与旧的等值语义一致,避免静默放行全部行)
856
+ Never,
857
+ }
858
+
859
+ /// 解析单个操作符。语义对齐 lib/table.js 的 `_matchOperator`。
860
+ pub(crate) fn parse_op<'a>(op: &str, target: &'a serde_json::Value) -> Cond<'a> {
861
+ match op {
862
+ "$eq" => Cond::Eq(target),
863
+ "$ne" => Cond::Ne(target),
864
+ "$gt" => Cond::Gt(target),
865
+ "$gte" => Cond::Gte(target),
866
+ "$lt" => Cond::Lt(target),
867
+ "$lte" => Cond::Lte(target),
868
+ "$in" => Cond::In(target),
869
+ "$nin" => Cond::Nin(target),
870
+ "$between" => Cond::Between(target),
871
+ "$exists" => Cond::Exists(target.as_bool().unwrap_or(true)),
872
+ // $like / $regex 目标必须是字符串;非法或编译失败的模式退化为「永不匹配」。
873
+ // regex-lite 为线性时间引擎,不存在灾难性回溯,无需 JS 侧 isSafeRegex 的长度拦截。
874
+ "$like" => match target.as_str() {
875
+ Some(p) => regex_lite::Regex::new(&like_to_regex(p))
876
+ .map(|re| Cond::Like(Box::new(re)))
877
+ .unwrap_or(Cond::Never),
878
+ None => Cond::Never,
879
+ },
880
+ "$regex" => match target.as_str() {
881
+ Some(p) => regex_lite::Regex::new(p)
882
+ .map(|re| Cond::Regex(Box::new(re)))
883
+ .unwrap_or(Cond::Never),
884
+ None => Cond::Never,
885
+ },
886
+ _ => Cond::Never,
887
+ }
888
+ }
889
+
890
+ /// 把 SQL LIKE 模式转成锚定的、大小写不敏感的正则:
891
+ /// 先转义正则元字符,再把 `%` 视作 `.*`、`_` 视作 `.`,
892
+ /// 与 lib/table.js 的 `$like` 实现逐字符对齐。
893
+ fn like_to_regex(pattern: &str) -> String {
894
+ let mut out = String::with_capacity(pattern.len() + 8);
895
+ out.push_str("(?i)^");
896
+ for ch in pattern.chars() {
897
+ match ch {
898
+ '%' => out.push_str(".*"),
899
+ '_' => out.push('.'),
900
+ '.' | '*' | '+' | '?' | '^' | '$' | '{' | '}' | '(' | ')' | '|' | '[' | ']' | '\\' => {
901
+ out.push('\\');
902
+ out.push(ch);
680
903
  }
904
+ c => out.push(c),
681
905
  }
906
+ }
907
+ out.push('$');
908
+ out
909
+ }
682
910
 
683
- let o = offset.unwrap_or(0);
684
- let l = limit.unwrap_or(usize::MAX);
685
- results.into_iter().skip(o).take(l).map(|r| self.to_row(r)).collect()
911
+ /// 把存储值转成 JS `String(value)` 的等价字符串(供 `$regex` 语义使用)。
912
+ fn field_value_to_string(v: &FieldValue) -> std::borrow::Cow<'_, str> {
913
+ use std::borrow::Cow;
914
+ match v {
915
+ FieldValue::Null => Cow::Borrowed("null"),
916
+ FieldValue::Bool(b) => Cow::Owned(b.to_string()),
917
+ FieldValue::Int(n) => Cow::Owned(n.to_string()),
918
+ FieldValue::Float(f) => Cow::Owned(f.to_string()),
919
+ FieldValue::String(s) => Cow::Borrowed(s),
920
+ }
921
+ }
922
+
923
+ #[inline]
924
+ pub(crate) fn matches_filter(vals: &[FieldValue], conds: &[(usize, Cond)]) -> bool {
925
+ // 最常见形态(单字段等值,如按主键点查)走直通,避免逐行做枚举分派
926
+ if let [(pos, Cond::Eq(t))] = conds {
927
+ return vals.get(*pos).map_or(false, |fv| fv.eq_json(t));
928
+ }
929
+ conds.iter().all(|(pos, c)| match_cond(vals.get(*pos), c))
930
+ }
931
+
932
+ #[inline]
933
+ fn match_cond(fv: Option<&FieldValue>, cond: &Cond) -> bool {
934
+ use std::cmp::Ordering;
935
+ let cmp = |t: &serde_json::Value, want: fn(Ordering) -> bool| {
936
+ fv.and_then(|v| cmp_to_json(v, t)).map_or(false, want)
937
+ };
938
+ match cond {
939
+ Cond::Never => false,
940
+ Cond::Exists(want) => fv.is_some() == *want,
941
+ Cond::Eq(t) => fv.map_or(false, |v| v.eq_json(t)),
942
+ Cond::Ne(t) => !fv.map_or(false, |v| v.eq_json(t)),
943
+ Cond::In(t) => match t.as_array() {
944
+ Some(arr) => fv.map_or(false, |v| arr.iter().any(|e| v.eq_json(e))),
945
+ None => false,
946
+ },
947
+ Cond::Nin(t) => match t.as_array() {
948
+ Some(arr) => !fv.map_or(false, |v| arr.iter().any(|e| v.eq_json(e))),
949
+ None => false,
950
+ },
951
+ Cond::Gt(t) => cmp(t, |o| o == Ordering::Greater),
952
+ Cond::Gte(t) => cmp(t, |o| o != Ordering::Less),
953
+ Cond::Lt(t) => cmp(t, |o| o == Ordering::Less),
954
+ Cond::Lte(t) => cmp(t, |o| o != Ordering::Greater),
955
+ Cond::Between(t) => match t.as_array() {
956
+ Some(arr) if arr.len() == 2 => {
957
+ cmp(&arr[0], |o| o != Ordering::Less) && cmp(&arr[1], |o| o != Ordering::Greater)
958
+ }
959
+ _ => false,
960
+ },
961
+ // $like 只对字符串字段生效(对齐 lib/table.js:非字符串直接 false)
962
+ Cond::Like(re) => matches!(fv, Some(FieldValue::String(s)) if re.is_match(s)),
963
+ // $regex 先把值字符串化再匹配(对齐 lib/table.js 的 String(value))
964
+ Cond::Regex(re) => fv.map_or(false, |v| re.is_match(&field_value_to_string(v))),
965
+ }
966
+ }
967
+
968
+ /// 存储值与 JSON 目标值比较;类型不可比或含 null/NaN 时返回 None(视为不匹配,
969
+ /// 对齐 JS 里 `value !== null && value > expected` 的写法)。
970
+ fn cmp_to_json(fv: &FieldValue, jv: &serde_json::Value) -> Option<std::cmp::Ordering> {
971
+ use serde_json::Value as J;
972
+ match (fv, jv) {
973
+ (FieldValue::Null, _) | (_, J::Null) => None,
974
+ (FieldValue::Int(a), J::Number(n)) => match n.as_i64() {
975
+ Some(b) => Some(a.cmp(&b)),
976
+ None => n.as_f64().and_then(|b| (*a as f64).partial_cmp(&b)),
977
+ },
978
+ (FieldValue::Float(a), J::Number(n)) => n.as_f64().and_then(|b| a.partial_cmp(&b)),
979
+ (FieldValue::Bool(a), J::Bool(b)) => Some(a.cmp(b)),
980
+ (FieldValue::String(a), J::String(b)) => Some(a.as_str().cmp(b.as_str())),
981
+ _ => None,
686
982
  }
687
983
  }
688
984