rypipe 2.1.0__tar.gz → 2.2.0__tar.gz

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 (65) hide show
  1. {rypipe-2.1.0 → rypipe-2.2.0}/Cargo.lock +6 -6
  2. {rypipe-2.1.0 → rypipe-2.2.0}/Cargo.toml +1 -1
  3. {rypipe-2.1.0 → rypipe-2.2.0}/PKG-INFO +1 -1
  4. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/Cargo.toml +1 -1
  5. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/columnar.rs +103 -60
  6. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/dict.rs +17 -11
  7. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/parallel.rs +36 -24
  8. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/data_integrity_test.rs +528 -0
  9. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/Cargo.toml +1 -1
  10. {rypipe-2.1.0 → rypipe-2.2.0}/pyproject.toml +1 -1
  11. {rypipe-2.1.0 → rypipe-2.2.0}/LICENSE +0 -0
  12. {rypipe-2.1.0 → rypipe-2.2.0}/README.md +0 -0
  13. {rypipe-2.1.0 → rypipe-2.2.0}/_rypipe/__init__.py +0 -0
  14. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/README.md +0 -0
  15. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/build.rs +0 -0
  16. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/examples/bench_blockmasks.rs +0 -0
  17. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/examples/bench_push_tier.rs +0 -0
  18. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/examples/bench_scanner_single.rs +0 -0
  19. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/examples/bench_throughput.rs +0 -0
  20. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/alloc_stats.rs +0 -0
  21. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/arrow_export.rs +0 -0
  22. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/bench/mod.rs +0 -0
  23. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/block_masks.rs +0 -0
  24. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/bounded.rs +0 -0
  25. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/consumer.rs +0 -0
  26. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/decoder.rs +0 -0
  27. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/engine/locate.rs +0 -0
  28. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/engine/mod.rs +0 -0
  29. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/engine/predicate.rs +0 -0
  30. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/engine/table_builder.rs +0 -0
  31. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/error.rs +0 -0
  32. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/input.rs +0 -0
  33. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/lib.rs +0 -0
  34. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/merge.rs +0 -0
  35. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/parallel_stream.rs +0 -0
  36. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/pipeline.rs +0 -0
  37. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/plan.rs +0 -0
  38. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/scan/mod.rs +0 -0
  39. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/schema.rs +0 -0
  40. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/streaming.rs +0 -0
  41. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/src/value.rs +0 -0
  42. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/adapter_contract_test.rs +0 -0
  43. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/bounded_executor_test.rs +0 -0
  44. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/filter_tree_bytes_test.rs +0 -0
  45. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/merge_error_test.rs +0 -0
  46. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/pushdown_test.rs +0 -0
  47. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-core/tests/schema_unify_test.rs +0 -0
  48. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/build.rs +0 -0
  49. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/src/export.rs +0 -0
  50. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/src/lib.rs +0 -0
  51. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/src/plan_kwargs.rs +0 -0
  52. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/tests/conftest.py +0 -0
  53. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/tests/test_rypipe_api.py +0 -0
  54. {rypipe-2.1.0 → rypipe-2.2.0}/crates/rypipe-python/tests/test_source_pipeline.py +0 -0
  55. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/__init__.py +0 -0
  56. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/batchpipe.py +0 -0
  57. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/fusion.py +0 -0
  58. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/pipeline.py +0 -0
  59. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/sinks.py +0 -0
  60. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/source.py +0 -0
  61. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/stages/__init__.py +0 -0
  62. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/stages/cast.py +0 -0
  63. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/stages/drop.py +0 -0
  64. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/stages/filter.py +0 -0
  65. {rypipe-2.1.0 → rypipe-2.2.0}/rypipe/stages/rename.py +0 -0
@@ -417,9 +417,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
417
417
 
418
418
  [[package]]
419
419
  name = "cc"
420
- version = "1.4.4"
420
+ version = "1.4.5"
421
421
  source = "registry+https://github.com/rust-lang/crates.io-index"
422
- checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
422
+ checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
423
423
  dependencies = [
424
424
  "find-msvc-tools",
425
425
  "jobserver",
@@ -518,9 +518,9 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
518
518
 
519
519
  [[package]]
520
520
  name = "find-msvc-tools"
521
- version = "0.1.11"
521
+ version = "0.1.12"
522
522
  source = "registry+https://github.com/rust-lang/crates.io-index"
523
- checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
523
+ checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
524
524
 
525
525
  [[package]]
526
526
  name = "flate2"
@@ -1058,7 +1058,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1058
1058
 
1059
1059
  [[package]]
1060
1060
  name = "rypipe-core"
1061
- version = "2.1.0"
1061
+ version = "2.2.0"
1062
1062
  dependencies = [
1063
1063
  "arrow 55.2.0",
1064
1064
  "chrono",
@@ -1078,7 +1078,7 @@ dependencies = [
1078
1078
 
1079
1079
  [[package]]
1080
1080
  name = "rypipe-python"
1081
- version = "2.1.0"
1081
+ version = "2.2.0"
1082
1082
  dependencies = [
1083
1083
  "arrow 59.3.0",
1084
1084
  "mimalloc",
@@ -3,7 +3,7 @@ members = ["crates/rypipe-core", "crates/rypipe-python"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "2.1.0"
6
+ version = "2.2.0"
7
7
  edition = "2021"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/emiliano-go/rypipe"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rypipe
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Requires-Dist: pyarrow>=15
5
5
  Requires-Dist: rypipe[pandas,polars] ; extra == 'all'
6
6
  Requires-Dist: pytest>=7 ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rypipe-core"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  description = "Format-agnostic ingestion framework with Arrow export"
@@ -454,20 +454,31 @@ pub(crate) enum ColumnBuilder {
454
454
  Timestamp(TimeUnit, PrimColumn<i64>),
455
455
  Dictionary {
456
456
  codes: NullableColumn<i32>,
457
- dict: Vec<String>,
457
+ /// Contiguous byte buffer for dictionary values (like StrColumn).
458
+ data: Vec<u8>,
459
+ /// Byte offsets into `data` for each dictionary entry.
460
+ offsets: Vec<i32>,
458
461
  /// value → code side-index.
459
- index: HashMap<String, i32>,
462
+ index: rustc_hash::FxHashMap<Box<str>, i32>,
460
463
  },
461
464
  }
462
465
 
463
466
  /// Look up `v` in the dictionary index, inserting a new code if absent.
464
- fn dict_code(dict: &mut Vec<String>, index: &mut HashMap<String, i32>, v: &str) -> i32 {
465
- if let Some(&code) = index.get(v) {
467
+ /// Uses a contiguous byte buffer for values instead of Vec<String>.
468
+ fn dict_code(
469
+ data: &mut Vec<u8>,
470
+ offsets: &mut Vec<i32>,
471
+ index: &mut rustc_hash::FxHashMap<Box<str>, i32>,
472
+ v: &str,
473
+ ) -> i32 {
474
+ let key: Box<str> = v.into();
475
+ if let Some(&code) = index.get(&key) {
466
476
  return code;
467
477
  }
468
- let code = dict.len() as i32;
469
- dict.push(v.to_owned());
470
- index.insert(v.to_owned(), code);
478
+ let code = offsets.len() as i32 - 1;
479
+ data.extend_from_slice(v.as_bytes());
480
+ offsets.push(data.len() as i32);
481
+ index.insert(key, code);
471
482
  code
472
483
  }
473
484
 
@@ -547,8 +558,9 @@ impl ColumnBuilder {
547
558
  }
548
559
  FieldType::Dictionary => ColumnBuilder::Dictionary {
549
560
  codes: NullableColumn::with_capacity(cap),
550
- dict: Vec::new(),
551
- index: HashMap::default(),
561
+ data: Vec::new(),
562
+ offsets: vec![0],
563
+ index: rustc_hash::FxHashMap::default(),
552
564
  },
553
565
  }
554
566
  }
@@ -567,8 +579,8 @@ impl ColumnBuilder {
567
579
  ColumnBuilder::Int64(v) => v.push(Some(i)),
568
580
  ColumnBuilder::Float64(v) => v.push(Some(i as f64)),
569
581
  ColumnBuilder::String(col) => col.push(Some(&i.to_string())),
570
- ColumnBuilder::Dictionary { codes, dict, index } => {
571
- let code = dict_code(dict, index, &i.to_string());
582
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => {
583
+ let code = dict_code(data, offsets, index, &i.to_string());
572
584
  codes.push(Some(code));
573
585
  }
574
586
  _ => self.push(None),
@@ -577,8 +589,8 @@ impl ColumnBuilder {
577
589
  ColumnBuilder::Float64(v) => v.push(Some(f)),
578
590
  ColumnBuilder::Int64(v) => v.push(Some(f as i64)),
579
591
  ColumnBuilder::String(col) => col.push(Some(&f.to_string())),
580
- ColumnBuilder::Dictionary { codes, dict, index } => {
581
- let code = dict_code(dict, index, &f.to_string());
592
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => {
593
+ let code = dict_code(data, offsets, index, &f.to_string());
582
594
  codes.push(Some(code));
583
595
  }
584
596
  _ => self.push(None),
@@ -586,8 +598,8 @@ impl ColumnBuilder {
586
598
  Value::Bool(b) => match self {
587
599
  ColumnBuilder::Boolean(v) => v.push(Some(b)),
588
600
  ColumnBuilder::String(col) => col.push(Some(&b.to_string())),
589
- ColumnBuilder::Dictionary { codes, dict, index } => {
590
- let code = dict_code(dict, index, &b.to_string());
601
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => {
602
+ let code = dict_code(data, offsets, index, &b.to_string());
591
603
  codes.push(Some(code));
592
604
  }
593
605
  _ => self.push(None),
@@ -597,8 +609,8 @@ impl ColumnBuilder {
597
609
  ColumnBuilder::Int64(v) => v.push(Some(d as i64)),
598
610
  ColumnBuilder::Float64(v) => v.push(Some(d as f64)),
599
611
  ColumnBuilder::String(col) => col.push(Some(&format_date32(d))),
600
- ColumnBuilder::Dictionary { codes, dict, index } => {
601
- let code = dict_code(dict, index, &format_date32(d));
612
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => {
613
+ let code = dict_code(data, offsets, index, &format_date32(d));
602
614
  codes.push(Some(code));
603
615
  }
604
616
  _ => self.push(None),
@@ -614,12 +626,12 @@ impl ColumnBuilder {
614
626
  Some(unit) => col.push(Some(&format_timestamp(ts, unit))),
615
627
  None => col.push(Some(&ts.to_string())),
616
628
  },
617
- ColumnBuilder::Dictionary { codes, dict, index } => {
629
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => {
618
630
  let text = match unit {
619
631
  Some(unit) => format_timestamp(ts, unit),
620
632
  None => ts.to_string(),
621
633
  };
622
- let code = dict_code(dict, index, &text);
634
+ let code = dict_code(data, offsets, index, &text);
623
635
  codes.push(Some(code));
624
636
  }
625
637
  _ => self.push(None),
@@ -697,16 +709,17 @@ impl ColumnBuilder {
697
709
  "cannot promote column to unified variant '{target}'"
698
710
  )));
699
711
  }
700
- let mut dict: Vec<String> = Vec::new();
701
- let mut index: HashMap<String, i32> = HashMap::default();
712
+ let mut data: Vec<u8> = Vec::new();
713
+ let mut offsets: Vec<i32> = vec![0];
714
+ let mut index: rustc_hash::FxHashMap<Box<str>, i32> = rustc_hash::FxHashMap::default();
702
715
  let mut codes = NullableColumn::with_capacity(old.len());
703
716
  for val in old.iter() {
704
717
  match val {
705
- Some(s) => codes.push(Some(dict_code(&mut dict, &mut index, s))),
718
+ Some(s) => codes.push(Some(dict_code(&mut data, &mut offsets, &mut index, s))),
706
719
  None => codes.push(None),
707
720
  }
708
721
  }
709
- *self = ColumnBuilder::Dictionary { codes, dict, index };
722
+ *self = ColumnBuilder::Dictionary { codes, data, offsets, index };
710
723
  return Ok(());
711
724
  }
712
725
 
@@ -735,9 +748,9 @@ impl ColumnBuilder {
735
748
  ColumnBuilder::Timestamp(unit, v) => {
736
749
  v.push(value.and_then(|s| parse_timestamp(&s, *unit)));
737
750
  }
738
- ColumnBuilder::Dictionary { codes, dict, index } => match value {
751
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => match value {
739
752
  Some(v) => {
740
- let idx = dict_code(dict, index, &v);
753
+ let idx = dict_code(data, offsets, index, &v);
741
754
  codes.push(Some(idx));
742
755
  }
743
756
  None => codes.push(None),
@@ -765,9 +778,9 @@ impl ColumnBuilder {
765
778
  ColumnBuilder::Timestamp(unit, v) => {
766
779
  v.push(value.and_then(|s| parse_timestamp(s, *unit)));
767
780
  }
768
- ColumnBuilder::Dictionary { codes, dict, index } => match value {
781
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => match value {
769
782
  Some(v) => {
770
- let idx = dict_code(dict, index, v);
783
+ let idx = dict_code(data, offsets, index, v);
771
784
  codes.push(Some(idx));
772
785
  }
773
786
  None => codes.push(None),
@@ -807,7 +820,7 @@ impl ColumnBuilder {
807
820
  ColumnBuilder::Boolean(v) => v.bytes_used(),
808
821
  ColumnBuilder::Date32(v) => v.bytes_used(),
809
822
  ColumnBuilder::Timestamp(_, v) => v.bytes_used(),
810
- ColumnBuilder::Dictionary { codes, dict, .. } => codes.len() * 4 + dict.len() * 16,
823
+ ColumnBuilder::Dictionary { codes, data, offsets, .. } => codes.len() * 4 + data.len() + offsets.len() * 4,
811
824
  }
812
825
  }
813
826
 
@@ -820,8 +833,8 @@ impl ColumnBuilder {
820
833
  ColumnBuilder::Boolean(v) => v.capacity_bytes(),
821
834
  ColumnBuilder::Date32(v) => v.capacity_bytes(),
822
835
  ColumnBuilder::Timestamp(_, v) => v.capacity_bytes(),
823
- ColumnBuilder::Dictionary { codes, dict, .. } => {
824
- codes.capacity_bytes() + dict.capacity() * 16
836
+ ColumnBuilder::Dictionary { codes, data, offsets, .. } => {
837
+ codes.capacity_bytes() + data.capacity() + offsets.capacity() * 4
825
838
  }
826
839
  }
827
840
  }
@@ -835,11 +848,12 @@ impl ColumnBuilder {
835
848
  ColumnBuilder::Boolean(v) => ColumnBuilder::Boolean(v.split_off(n)),
836
849
  ColumnBuilder::Date32(v) => ColumnBuilder::Date32(v.split_off(n)),
837
850
  ColumnBuilder::Timestamp(unit, v) => ColumnBuilder::Timestamp(*unit, v.split_off(n)),
838
- ColumnBuilder::Dictionary { codes, dict, index } => {
851
+ ColumnBuilder::Dictionary { codes, data, offsets, index } => {
839
852
  let other_codes = codes.split_off(n);
840
853
  ColumnBuilder::Dictionary {
841
854
  codes: other_codes,
842
- dict: dict.clone(),
855
+ data: data.clone(),
856
+ offsets: offsets.clone(),
843
857
  index: index.clone(),
844
858
  }
845
859
  }
@@ -852,8 +866,12 @@ impl ColumnBuilder {
852
866
  pub(crate) fn get_filter_view(&self, index: usize) -> Option<&str> {
853
867
  match self {
854
868
  ColumnBuilder::String(v) => v.get(index),
855
- ColumnBuilder::Dictionary { codes, dict, .. } => {
856
- codes.get(index).map(|code| dict[*code as usize].as_str())
869
+ ColumnBuilder::Dictionary { codes, data, offsets, .. } => {
870
+ codes.get(index).map(|code| {
871
+ let start = offsets[*code as usize] as usize;
872
+ let end = offsets[*code as usize + 1] as usize;
873
+ std::str::from_utf8(&data[start..end]).unwrap_or("")
874
+ })
857
875
  }
858
876
  _ => None,
859
877
  }
@@ -872,8 +890,12 @@ impl ColumnBuilder {
872
890
  let unit = *unit;
873
891
  v.get(index).map(|ts| format_timestamp(ts, unit))
874
892
  }
875
- ColumnBuilder::Dictionary { codes, dict, .. } => {
876
- codes.get(index).map(|code| dict[*code as usize].clone())
893
+ ColumnBuilder::Dictionary { codes, data, offsets, .. } => {
894
+ codes.get(index).map(|code| {
895
+ let start = offsets[*code as usize] as usize;
896
+ let end = offsets[*code as usize + 1] as usize;
897
+ String::from_utf8_lossy(&data[start..end]).into_owned()
898
+ })
877
899
  }
878
900
  }
879
901
  }
@@ -888,9 +910,13 @@ impl ColumnBuilder {
888
910
  ColumnBuilder::Boolean(v) => v.get(index).map(TypedValue::Bool),
889
911
  ColumnBuilder::Date32(v) => v.get(index).map(TypedValue::Date32),
890
912
  ColumnBuilder::Timestamp(_, v) => v.get(index).map(TypedValue::Timestamp),
891
- ColumnBuilder::Dictionary { codes, dict, .. } => codes
913
+ ColumnBuilder::Dictionary { codes, data, offsets, .. } => codes
892
914
  .get(index)
893
- .map(|&idx| TypedValue::Str(dict[idx as usize].as_str())),
915
+ .map(|&idx| {
916
+ let start = offsets[idx as usize] as usize;
917
+ let end = offsets[idx as usize + 1] as usize;
918
+ TypedValue::Str(std::str::from_utf8(&data[start..end]).unwrap_or(""))
919
+ }),
894
920
  }
895
921
  }
896
922
 
@@ -925,20 +951,25 @@ impl ColumnBuilder {
925
951
  (
926
952
  ColumnBuilder::Dictionary {
927
953
  codes: a_codes,
928
- dict: a_dict,
954
+ data: a_data,
955
+ offsets: a_offsets,
929
956
  index: a_index,
930
957
  },
931
958
  ColumnBuilder::Dictionary {
932
959
  codes: b_codes,
933
- dict: b_dict,
960
+ data: b_data,
961
+ offsets: b_offsets,
934
962
  ..
935
963
  },
936
964
  ) => {
937
965
  // Remap b's dictionary into a's once, then translate codes.
938
- let remap: Vec<i32> = b_dict
939
- .iter()
940
- .map(|val| dict_code(a_dict, a_index, val))
941
- .collect();
966
+ let mut remap: Vec<i32> = Vec::with_capacity(b_offsets.len() - 1);
967
+ for i in 0..(b_offsets.len() - 1) {
968
+ let start = b_offsets[i] as usize;
969
+ let end = b_offsets[i + 1] as usize;
970
+ let val = std::str::from_utf8(&b_data[start..end]).unwrap_or("");
971
+ remap.push(dict_code(a_data, a_offsets, a_index, val));
972
+ }
942
973
  for c in b_codes.iter() {
943
974
  a_codes.push(c.map(|code| remap[*code as usize]));
944
975
  }
@@ -981,19 +1012,20 @@ impl ColumnBuilder {
981
1012
  return;
982
1013
  }
983
1014
  // Upgrade: build dictionary + codes.
984
- let mut dict: Vec<String> = Vec::new();
985
- let mut index: HashMap<String, i32> = HashMap::default();
1015
+ let mut data: Vec<u8> = Vec::new();
1016
+ let mut offsets: Vec<i32> = vec![0];
1017
+ let mut index: rustc_hash::FxHashMap<Box<str>, i32> = rustc_hash::FxHashMap::default();
986
1018
  let mut codes = NullableColumn::with_capacity(old.len());
987
1019
  for v in old.iter() {
988
1020
  match v {
989
1021
  Some(s) => {
990
- let idx = dict_code(&mut dict, &mut index, s);
1022
+ let idx = dict_code(&mut data, &mut offsets, &mut index, s);
991
1023
  codes.push(Some(idx));
992
1024
  }
993
1025
  None => codes.push(None),
994
1026
  }
995
1027
  }
996
- *self = ColumnBuilder::Dictionary { codes, dict, index };
1028
+ *self = ColumnBuilder::Dictionary { codes, data, offsets, index };
997
1029
  }
998
1030
 
999
1031
  /// Mutable access to dictionary codes for in-place remap.
@@ -1009,11 +1041,13 @@ impl ColumnBuilder {
1009
1041
  /// No-op if not a Dictionary variant.
1010
1042
  pub(crate) fn replace_dict(
1011
1043
  &mut self,
1012
- new_dict: Vec<String>,
1013
- new_index: rustc_hash::FxHashMap<String, i32>,
1044
+ new_data: Vec<u8>,
1045
+ new_offsets: Vec<i32>,
1046
+ new_index: rustc_hash::FxHashMap<Box<str>, i32>,
1014
1047
  ) {
1015
- if let ColumnBuilder::Dictionary { dict, index, .. } = self {
1016
- *dict = new_dict;
1048
+ if let ColumnBuilder::Dictionary { data, offsets, index, .. } = self {
1049
+ *data = new_data;
1050
+ *offsets = new_offsets;
1017
1051
  *index = new_index;
1018
1052
  }
1019
1053
  }
@@ -1100,13 +1134,14 @@ impl ColumnBuilder {
1100
1134
  TimeUnit::Microsecond => v.to_arrow::<TimestampMicrosecondType>()?,
1101
1135
  TimeUnit::Nanosecond => v.to_arrow::<TimestampNanosecondType>()?,
1102
1136
  },
1103
- ColumnBuilder::Dictionary { codes, dict, .. } => {
1137
+ ColumnBuilder::Dictionary { codes, data, offsets, .. } => {
1138
+ use arrow::buffer::{Buffer, OffsetBuffer, ScalarBuffer};
1104
1139
  let codes_arr: Int32Array = std::mem::take(codes).into_options().collect();
1105
- let values: ArrayRef = Arc::new(
1106
- dict.iter()
1107
- .map(|s| Some(s.as_str()))
1108
- .collect::<StringArray>(),
1109
- );
1140
+ // Zero-copy: wrap contiguous data+offsets directly into StringArray.
1141
+ // Dictionary values are always non-null, so validity is None.
1142
+ let offsets_buf = OffsetBuffer::new(ScalarBuffer::from(std::mem::take(offsets)));
1143
+ let data_buf = Buffer::from_vec(std::mem::take(data));
1144
+ let values: ArrayRef = Arc::new(StringArray::try_new(offsets_buf, data_buf, None)?);
1110
1145
  let arr = DictionaryArray::<Int32Type>::try_new(codes_arr, values)?;
1111
1146
  Arc::new(arr)
1112
1147
  }
@@ -1277,8 +1312,16 @@ mod tests {
1277
1312
  d.push_value(Value::Float64(7.0)); // same string as Int64(7)
1278
1313
  d.push_value(Value::Bool(true));
1279
1314
  d.push_value(Value::Null);
1280
- if let ColumnBuilder::Dictionary { codes, dict, .. } = &d {
1281
- assert_eq!(dict, &["7", "true"]);
1315
+ if let ColumnBuilder::Dictionary { codes, data, offsets, .. } = &d {
1316
+ // Verify dictionary values via the contiguous buffer
1317
+ let vals: Vec<&str> = (0..offsets.len() - 1)
1318
+ .map(|i| {
1319
+ let start = offsets[i] as usize;
1320
+ let end = offsets[i + 1] as usize;
1321
+ std::str::from_utf8(&data[start..end]).unwrap()
1322
+ })
1323
+ .collect();
1324
+ assert_eq!(vals, vec!["7", "true"]);
1282
1325
  assert_eq!(
1283
1326
  codes.iter().map(|o| o.copied()).collect::<Vec<_>>(),
1284
1327
  vec![Some(0), Some(0), Some(1), None]
@@ -74,7 +74,7 @@ pub struct DictValues {
74
74
 
75
75
  /// Unify per-chunk dictionaries with the seed.
76
76
  ///
77
- /// Works with `ColumnBuilder::Dictionary { codes, dict, index }`.
77
+ /// Works with `ColumnBuilder::Dictionary { codes, data, offsets, index }`.
78
78
  /// Seed codes are `0..seed.len()`, overflow are `seed.len()..`.
79
79
  pub(crate) fn unify_dictionaries(
80
80
  seed: &SeedDict,
@@ -91,12 +91,15 @@ pub(crate) fn unify_dictionaries(
91
91
 
92
92
  // Collect overflow values not in seed
93
93
  for col in locals {
94
- if let ColumnBuilder::Dictionary { dict, .. } = col {
95
- for s in dict.iter().skip(seed.len()) {
96
- let k: Box<str> = s.clone().into_boxed_str();
94
+ if let ColumnBuilder::Dictionary { data, offsets, .. } = col {
95
+ for i in seed.len()..(offsets.len() - 1) {
96
+ let start = offsets[i] as usize;
97
+ let end = offsets[i + 1] as usize;
98
+ let val = std::str::from_utf8(&data[start..end]).unwrap_or("");
99
+ let k: Box<str> = val.into();
97
100
  if !global_index.contains_key(&k) {
98
101
  global_index.insert(k.clone(), next_code);
99
- global_data.extend_from_slice(s.as_bytes());
102
+ global_data.extend_from_slice(val.as_bytes());
100
103
  global_offsets.push(global_data.len() as i32);
101
104
  next_code += 1;
102
105
  }
@@ -112,12 +115,15 @@ pub(crate) fn unify_dictionaries(
112
115
  // Build remap tables: local code -> global code
113
116
  let mut remaps = Vec::with_capacity(locals.len());
114
117
  for col in locals {
115
- if let ColumnBuilder::Dictionary { dict, index, .. } = col {
116
- let local_len = dict.len();
118
+ if let ColumnBuilder::Dictionary { data, offsets, index, .. } = col {
119
+ let local_len = offsets.len() - 1;
117
120
  let mut map = Vec::with_capacity(local_len);
118
121
  let mut is_identity = true;
119
- for s in dict {
120
- let k: Box<str> = s.clone().into_boxed_str();
122
+ for i in 0..local_len {
123
+ let start = offsets[i] as usize;
124
+ let end = offsets[i + 1] as usize;
125
+ let val = std::str::from_utf8(&data[start..end]).unwrap_or("");
126
+ let k: Box<str> = val.into();
121
127
  let g = *global_index.get(&k).unwrap();
122
128
  let local = *index.get(k.as_ref()).unwrap();
123
129
  if g != local {
@@ -126,9 +132,9 @@ pub(crate) fn unify_dictionaries(
126
132
  map.push(g);
127
133
  }
128
134
  // If no overflow, it's essentially identity if dict == seed
129
- if dict.len() == seed.len() && is_identity {
135
+ if local_len == seed.len() && is_identity {
130
136
  // All seed, identity
131
- } else if dict.len() > seed.len() {
137
+ } else if local_len > seed.len() {
132
138
  is_identity = false;
133
139
  }
134
140
  remaps.push(RemapTable { map, is_identity });
@@ -153,21 +153,21 @@ impl ParallelExecutor {
153
153
  let first_idx = engines[0].field_index.get(col_name).copied();
154
154
  if let Some(idx) = first_idx {
155
155
  if engines[0].columns[idx].variant_key() == "dictionary" {
156
- let first_dict =
157
- if let crate::columnar::ColumnBuilder::Dictionary { dict, .. } =
156
+ let first_data =
157
+ if let crate::columnar::ColumnBuilder::Dictionary { data, offsets, .. } =
158
158
  &engines[0].columns[idx]
159
159
  {
160
- dict
160
+ (data, offsets)
161
161
  } else {
162
162
  continue;
163
163
  };
164
164
  for e in &engines[1..] {
165
165
  if let Some(&j) = e.field_index.get(col_name) {
166
166
  if let crate::columnar::ColumnBuilder::Dictionary {
167
- dict, ..
167
+ data, offsets, ..
168
168
  } = &e.columns[j]
169
169
  {
170
- if dict != first_dict {
170
+ if data != first_data.0 || offsets != first_data.1 {
171
171
  unify_col = Some(col_name.clone());
172
172
  break;
173
173
  }
@@ -185,42 +185,54 @@ impl ParallelExecutor {
185
185
  }
186
186
  let col_name = unify_col.unwrap();
187
187
  // Build seed from first chunk, unify, remap.
188
- let first_dict =
189
- if let crate::columnar::ColumnBuilder::Dictionary { dict, .. } =
188
+ let first_data =
189
+ if let crate::columnar::ColumnBuilder::Dictionary { data, offsets, .. } =
190
190
  &engines[0].columns[engines[0].field_index[&col_name]]
191
191
  {
192
- dict.clone()
192
+ (data.clone(), offsets.clone())
193
193
  } else {
194
194
  return engines_to_record_batches(engines, &plan);
195
195
  };
196
- let seed = crate::dict::SeedDict::new(first_dict);
196
+ // Build SeedDict from the contiguous buffer
197
+ let mut seed_values = Vec::new();
198
+ let mut seed_offsets = Vec::with_capacity(first_data.1.len());
199
+ seed_offsets.push(0);
200
+ for i in 0..(first_data.1.len() - 1) {
201
+ let start = first_data.1[i] as usize;
202
+ let end = first_data.1[i + 1] as usize;
203
+ seed_values.extend_from_slice(&first_data.0[start..end]);
204
+ seed_offsets.push(seed_values.len() as i32);
205
+ }
206
+ let seed = crate::dict::SeedDict {
207
+ values: seed_values,
208
+ offsets: seed_offsets,
209
+ index: rustc_hash::FxHashMap::default(), // Will be populated by unify
210
+ };
197
211
  let col_refs: Vec<&crate::columnar::ColumnBuilder> = engines
198
212
  .iter()
199
213
  .filter_map(|e| e.field_index.get(&col_name).map(|&idx| &e.columns[idx]))
200
214
  .collect();
201
215
  let (unified_dict, remaps) = crate::dict::unify_dictionaries(&seed, &col_refs);
202
- // Build unified dict values and index for replace_dict.
203
- let mut new_dict = Vec::with_capacity(unified_dict.offsets.len() - 1);
204
- for w in unified_dict.offsets.windows(2) {
205
- let start = w[0] as usize;
206
- let end = w[1] as usize;
207
- let s = std::str::from_utf8(&unified_dict.data[start..end])
208
- .unwrap_or("")
209
- .to_owned();
210
- new_dict.push(s);
216
+ // Build unified data+offsets+index for replace_dict.
217
+ let mut new_index: rustc_hash::FxHashMap<Box<str>, i32> = rustc_hash::FxHashMap::default();
218
+ for i in 0..(unified_dict.offsets.len() - 1) {
219
+ let start = unified_dict.offsets[i] as usize;
220
+ let end = unified_dict.offsets[i + 1] as usize;
221
+ let s = std::str::from_utf8(&unified_dict.data[start..end]).unwrap_or("");
222
+ let k: Box<str> = s.into();
223
+ new_index.insert(k, i as i32);
211
224
  }
212
- let new_index: HashMap<String, i32> = new_dict
213
- .iter()
214
- .enumerate()
215
- .map(|(i, s)| (s.clone(), i as i32))
216
- .collect();
217
225
  // Apply remap to each chunk, then replace dict.
218
226
  for (e, remap) in engines.iter_mut().zip(remaps.iter()) {
219
227
  if let Some(idx) = e.field_index.get(&col_name).copied() {
220
228
  if let Some(codes) = e.columns[idx].dict_codes_mut() {
221
229
  codes.remap_codes(&remap.map);
222
230
  }
223
- e.columns[idx].replace_dict(new_dict.clone(), new_index.clone());
231
+ e.columns[idx].replace_dict(
232
+ unified_dict.data.clone(),
233
+ unified_dict.offsets.clone(),
234
+ new_index.clone(),
235
+ );
224
236
  }
225
237
  }
226
238
  return engines_to_record_batches(engines, &plan);