Skip to content

Commit 00c21e2

Browse files
authored
Use a BTreeSet for struct fields to assure stable hashing (#458)
1 parent 17886f3 commit 00c21e2

File tree

5 files changed

+71
-64
lines changed

5 files changed

+71
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Added
1212

1313
### Fixed
14+
- partiql-types: Fixed handling of struct fields to be resilient to field order w.r.t. equality and hashing
1415

1516
## [0.7.1] - 2024-03-15
1617
### Changed

Cargo.toml

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,33 @@
22
authors = ["PartiQL Team <[email protected]>"]
33
homepage = "https://github.com/partiql/partiql-lang-rust"
44
repository = "https://github.com/partiql/partiql-lang-rust"
5-
version = "0.7.1"
5+
version = "0.7.2"
66
edition = "2021"
77

88
[workspace]
99
resolver = "2"
1010

1111
members = [
12-
"partiql",
13-
"partiql-ast",
14-
"partiql-ast/partiql-ast-macros",
15-
"partiql-ast-passes",
16-
"partiql-catalog",
17-
"partiql-conformance-tests",
18-
"partiql-conformance-test-generator",
19-
"partiql-source-map",
20-
"partiql-logical-planner",
21-
"partiql-logical",
22-
"partiql-eval",
23-
"partiql-ir",
24-
"partiql-irgen",
25-
"partiql-parser",
26-
"partiql-rewriter",
27-
"partiql-types",
28-
"partiql-value",
29-
30-
"extension/partiql-extension-ion",
31-
"extension/partiql-extension-ion-functions",
32-
"extension/partiql-extension-visualize",
12+
"partiql",
13+
"partiql-ast",
14+
"partiql-ast/partiql-ast-macros",
15+
"partiql-ast-passes",
16+
"partiql-catalog",
17+
"partiql-conformance-tests",
18+
"partiql-conformance-test-generator",
19+
"partiql-source-map",
20+
"partiql-logical-planner",
21+
"partiql-logical",
22+
"partiql-eval",
23+
"partiql-ir",
24+
"partiql-irgen",
25+
"partiql-parser",
26+
"partiql-rewriter",
27+
"partiql-types",
28+
"partiql-value",
29+
"extension/partiql-extension-ion",
30+
"extension/partiql-extension-ion-functions",
31+
"extension/partiql-extension-visualize",
3332
]
3433

3534
[profile.dev.build-override]

partiql-logical-planner/src/typer.rs

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,13 @@ impl<'c> PlanTyper<'c> {
202202
}
203203
}
204204
BindingsOp::Project(partiql_logical::Project { exprs }) => {
205-
let mut fields = vec![];
206-
for (k, v) in exprs {
205+
let fields = exprs.iter().map(|(k, v)| {
207206
self.type_vexpr(v, LookupOrder::LocalGlobal);
208-
209-
fields.push(StructField::new(
210-
k.as_str(),
211-
self.get_singleton_type_from_env(),
212-
));
213-
}
207+
StructField::new(k.as_str(), self.get_singleton_type_from_env())
208+
});
214209

215210
let ty = PartiqlType::new_struct(StructType::new(BTreeSet::from([
216-
StructConstraint::Fields(fields),
211+
StructConstraint::Fields(fields.collect()),
217212
])));
218213

219214
let derived_type_ctx = self.local_type_ctx();
@@ -598,11 +593,12 @@ mod tests {
598593
"SELECT customers.id, customers.name FROM customers",
599594
create_customer_schema(
600595
false,
601-
vec![
596+
[
602597
StructField::new("id", int!()),
603598
StructField::new("name", str!()),
604599
StructField::new("age", any!()),
605-
],
600+
]
601+
.into(),
606602
),
607603
vec![
608604
StructField::new("id", int!()),
@@ -617,11 +613,12 @@ mod tests {
617613
"SELECT id, customers.name FROM customers",
618614
create_customer_schema(
619615
false,
620-
vec![
616+
[
621617
StructField::new("id", int!()),
622618
StructField::new("name", str!()),
623619
StructField::new("age", any!()),
624-
],
620+
]
621+
.into(),
625622
),
626623
vec![
627624
StructField::new("id", int!()),
@@ -636,11 +633,12 @@ mod tests {
636633
"SELECT customers.id, customers.name, customers.age FROM customers",
637634
create_customer_schema(
638635
true,
639-
vec![
636+
[
640637
StructField::new("id", int!()),
641638
StructField::new("name", str!()),
642639
StructField::new("age", any!()),
643-
],
640+
]
641+
.into(),
644642
),
645643
vec![
646644
StructField::new("id", int!()),
@@ -655,10 +653,11 @@ mod tests {
655653
"SELECT customers.id, customers.name, customers.age FROM customers",
656654
create_customer_schema(
657655
false,
658-
vec![
656+
[
659657
StructField::new("id", int!()),
660658
StructField::new("name", str!()),
661-
],
659+
]
660+
.into(),
662661
),
663662
vec![
664663
StructField::new("id", int!()),
@@ -677,11 +676,12 @@ mod tests {
677676
"SELECT customers.id, customers.name, customers.details.age FROM customers",
678677
create_customer_schema(
679678
true,
680-
vec![
679+
[
681680
StructField::new("id", int!()),
682681
StructField::new("name", str!()),
683682
StructField::new("details", details.clone()),
684-
],
683+
]
684+
.into(),
685685
),
686686
vec![
687687
StructField::new("id", int!()),
@@ -695,19 +695,19 @@ mod tests {
695695
assert_query_typing(
696696
TypingMode::Strict,
697697
"SELECT customers.id, customers.name, customers.details.age, customers.details.foo.bar FROM customers",
698-
create_customer_schema(true,vec![
698+
create_customer_schema(true, [
699699
StructField::new("id", int!()),
700700
StructField::new("name", str!()),
701701
StructField::new("details", details.clone()),
702-
]),
702+
].into()),
703703
vec![
704704
StructField::new("id", int!()),
705705
StructField::new("name", str!()),
706706
StructField::new("age", int!()),
707707
StructField::new("bar", any!()),
708708
],
709709
)
710-
.expect("Type");
710+
.expect("Type");
711711
}
712712

713713
#[test]
@@ -723,11 +723,12 @@ mod tests {
723723
"SELECT d.age FROM customers.details AS d",
724724
create_customer_schema(
725725
true,
726-
vec![
726+
[
727727
StructField::new("id", int!()),
728728
StructField::new("name", str!()),
729729
StructField::new("details", details.clone()),
730-
],
730+
]
731+
.into(),
731732
),
732733
vec![StructField::new("age", int!())],
733734
)
@@ -739,11 +740,12 @@ mod tests {
739740
"SELECT c.id AS my_id, customers.name AS my_name FROM customers AS c",
740741
create_customer_schema(
741742
false,
742-
vec![
743+
[
743744
StructField::new("id", int!()),
744745
StructField::new("name", str!()),
745746
StructField::new("age", any!()),
746-
],
747+
]
748+
.into(),
747749
),
748750
vec![
749751
StructField::new("my_id", int!()),
@@ -756,18 +758,19 @@ mod tests {
756758
#[test]
757759
fn simple_sfw_err() {
758760
// Closed Schema with `Strict` typing mode and `age` non-existent projection.
759-
let err1 = r#"No Typing Information for SymbolPrimitive { value: "age", case: CaseInsensitive } in closed Schema PartiqlType(Struct(StructType { constraints: {Open(false), Fields([StructField { name: "id", ty: PartiqlType(Int) }, StructField { name: "name", ty: PartiqlType(String) }])} }))"#;
761+
let err1 = r#"No Typing Information for SymbolPrimitive { value: "age", case: CaseInsensitive } in closed Schema PartiqlType(Struct(StructType { constraints: {Open(false), Fields({StructField { name: "id", ty: PartiqlType(Int) }, StructField { name: "name", ty: PartiqlType(String) }})} }))"#;
760762

761763
assert_err(
762764
assert_query_typing(
763765
TypingMode::Strict,
764766
"SELECT customers.id, customers.name, customers.age FROM customers",
765767
create_customer_schema(
766768
false,
767-
vec![
769+
[
768770
StructField::new("id", int!()),
769771
StructField::new("name", str!()),
770-
],
772+
]
773+
.into(),
771774
),
772775
vec![],
773776
),
@@ -776,11 +779,12 @@ mod tests {
776779
// TypingError::IllegalState(err2.to_string()),
777780
],
778781
Some(bag![r#struct![BTreeSet::from([StructConstraint::Fields(
779-
vec![
782+
[
780783
StructField::new("id", int!()),
781784
StructField::new("name", str!()),
782785
StructField::new("age", undefined!()),
783786
]
787+
.into()
784788
),])]]),
785789
);
786790

@@ -791,7 +795,7 @@ mod tests {
791795
StructConstraint::Open(false)
792796
])];
793797

794-
let err1 = r#"No Typing Information for SymbolPrimitive { value: "details", case: CaseInsensitive } in closed Schema PartiqlType(Struct(StructType { constraints: {Open(false), Fields([StructField { name: "age", ty: PartiqlType(Int) }])} }))"#;
798+
let err1 = r#"No Typing Information for SymbolPrimitive { value: "details", case: CaseInsensitive } in closed Schema PartiqlType(Struct(StructType { constraints: {Open(false), Fields({StructField { name: "age", ty: PartiqlType(Int) }})} }))"#;
795799
let err2 = r"Illegal Derive Type PartiqlType(Undefined)";
796800

797801
assert_err(
@@ -800,11 +804,12 @@ mod tests {
800804
"SELECT customers.id, customers.name, customers.details.bar FROM customers",
801805
create_customer_schema(
802806
false,
803-
vec![
807+
[
804808
StructField::new("id", int!()),
805809
StructField::new("name", str!()),
806810
StructField::new("details", details),
807-
],
811+
]
812+
.into(),
808813
),
809814
vec![],
810815
),
@@ -813,11 +818,12 @@ mod tests {
813818
TypingError::IllegalState(err2.to_string()),
814819
],
815820
Some(bag![r#struct![BTreeSet::from([StructConstraint::Fields(
816-
vec![
821+
[
817822
StructField::new("id", int!()),
818823
StructField::new("name", str!()),
819824
StructField::new("bar", undefined!()),
820825
]
826+
.into()
821827
),])]]),
822828
);
823829
}
@@ -836,14 +842,14 @@ mod tests {
836842
e,
837843
TypeErr {
838844
errors: expected_errors,
839-
output
845+
output,
840846
}
841847
);
842848
}
843849
};
844850
}
845851

846-
fn create_customer_schema(is_open: bool, fields: Vec<StructField>) -> PartiqlType {
852+
fn create_customer_schema(is_open: bool, fields: BTreeSet<StructField>) -> PartiqlType {
847853
bag![r#struct![BTreeSet::from([
848854
StructConstraint::Fields(fields),
849855
StructConstraint::Open(is_open)
@@ -856,6 +862,7 @@ mod tests {
856862
schema: PartiqlType,
857863
expected_fields: Vec<StructField>,
858864
) -> Result<(), TypeErr> {
865+
let expected_fields: BTreeSet<_> = expected_fields.into_iter().collect();
859866
let actual = type_query(mode, query, TypeEnvEntry::new("customers", &[], schema));
860867

861868
match actual {

partiql-types/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ macro_rules! r#struct {
109109
#[macro_export]
110110
macro_rules! struct_fields {
111111
($(($x:expr, $y:expr)),+ $(,)?) => (
112-
$crate::StructConstraint::Fields(vec![$(($x, $y).into()),+])
112+
$crate::StructConstraint::Fields([$(($x, $y).into()),+].into())
113113
);
114114
}
115115

@@ -397,14 +397,14 @@ impl StructType {
397397
}
398398

399399
#[must_use]
400-
pub fn fields(&self) -> Vec<StructField> {
400+
pub fn fields(&self) -> BTreeSet<StructField> {
401401
self.constraints
402402
.iter()
403403
.flat_map(|c| {
404404
if let StructConstraint::Fields(fields) = c.clone() {
405405
fields
406406
} else {
407-
vec![]
407+
Default::default()
408408
}
409409
})
410410
.collect()
@@ -428,7 +428,7 @@ pub enum StructConstraint {
428428
Open(bool),
429429
Ordered(bool),
430430
DuplicateAttrs(bool),
431-
Fields(Vec<StructField>),
431+
Fields(BTreeSet<StructField>),
432432
}
433433

434434
#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]

partiql/src/subquery_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ mod tests {
8989
let res = evaluate(&catalog, plan, bindings, &[]).expect("should eval correctly");
9090
dbg!(&res);
9191
assert!(res != Value::Missing);
92-
assert_eq!(res, Value::from(bag![tuple![("a", "b")]]))
92+
assert_eq!(res, Value::from(bag![tuple![("a", "b")]]));
9393
}
9494

9595
#[test]
@@ -150,6 +150,6 @@ mod tests {
150150
let res = evaluate(&catalog, plan, bindings, &[]).expect("should eval correctly");
151151
dbg!(&res);
152152
assert!(res != Value::Missing);
153-
assert_eq!(res, Value::from(bag![tuple![("a", "b")]]))
153+
assert_eq!(res, Value::from(bag![tuple![("a", "b")]]));
154154
}
155155
}

0 commit comments

Comments
 (0)