Skip to content

Commit eef1cf7

Browse files
authored
Separate business and exchange types (#419)
### What We want to be able to confidently evolve the configuration format without risk of accidentally changing the behavior of previous versions. This PR has no user-observable change in behavior. Introducing a new major version of the configuration format is now a much more well-defined task, which should consist most of just duplicating the entire `version3` module hierarchy and then introducing the new changes in the duplicated copy. ### How In order to achieve this we duplicate the complete metadata type hierarchy into configuration crate and introduce functions that convert between the now separate exchange types and business types, and ensure that all logic that has to do with serialization only lives with the exchange types (the version types)
1 parent 9225966 commit eef1cf7

File tree

16 files changed

+1337
-388
lines changed

16 files changed

+1337
-388
lines changed

Cargo.lock

Lines changed: 1 addition & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/configuration/src/configuration.rs

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ use std::path::Path;
44

55
use schemars::JsonSchema;
66
use serde::{Deserialize, Serialize};
7-
use tokio::fs;
87

98
use query_engine_metadata::metadata;
109

1110
use crate::environment::Environment;
1211
use crate::error::Error;
13-
use crate::values::{ConnectionUri, IsolationLevel, PoolSettings, Secret};
12+
use crate::values::{IsolationLevel, PoolSettings};
1413
use crate::version3;
1514

1615
pub const CONFIGURATION_FILENAME: &str = "configuration.json";
@@ -59,45 +58,6 @@ pub async fn parse_configuration(
5958
configuration_dir: impl AsRef<Path>,
6059
environment: impl Environment,
6160
) -> Result<Configuration, Error> {
62-
let configuration_file = configuration_dir.as_ref().join(CONFIGURATION_FILENAME);
63-
64-
let configuration_file_contents =
65-
fs::read_to_string(&configuration_file)
66-
.await
67-
.map_err(|err| {
68-
Error::IoErrorButStringified(format!("{}: {}", &configuration_file.display(), err))
69-
})?;
70-
let mut configuration: version3::RawConfiguration =
71-
serde_json::from_str(&configuration_file_contents).map_err(|error| Error::ParseError {
72-
file_path: configuration_file.clone(),
73-
line: error.line(),
74-
column: error.column(),
75-
message: error.to_string(),
76-
})?;
77-
// look for native query sql file references and read from disk.
78-
for native_query_sql in configuration.metadata.native_queries.0.values_mut() {
79-
native_query_sql.sql = metadata::NativeQuerySqlEither::NativeQuerySql(
80-
native_query_sql
81-
.sql
82-
.from_external(configuration_dir.as_ref())
83-
.map_err(Error::IoErrorButStringified)?,
84-
);
85-
}
86-
let connection_uri =
87-
match configuration.connection_settings.connection_uri {
88-
ConnectionUri(Secret::Plain(uri)) => Ok(uri),
89-
ConnectionUri(Secret::FromEnvironment { variable }) => environment
90-
.read(&variable)
91-
.map_err(|error| Error::MissingEnvironmentVariable {
92-
file_path: configuration_file,
93-
message: error.to_string(),
94-
}),
95-
}?;
96-
Ok(Configuration {
97-
metadata: configuration.metadata,
98-
pool_settings: configuration.connection_settings.pool_settings,
99-
connection_uri,
100-
isolation_level: configuration.connection_settings.isolation_level,
101-
mutations_version: configuration.mutations_version,
102-
})
61+
// Try parsing each supported version in turn
62+
version3::parse_configuration(configuration_dir, environment).await
10363
}

crates/configuration/src/version3/comparison.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Helpers for the comparison operators configuration.
22
3-
use query_engine_metadata::metadata::database::OperatorKind;
3+
use super::database::OperatorKind;
44
use schemars::JsonSchema;
55
use serde::{Deserialize, Serialize};
66

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
//! Metadata information regarding the database and tracked information.
2+
3+
// This code was copied from a different place that predated the introduction of clippy to the
4+
// project. Therefore we disregard certain clippy lints:
5+
#![allow(
6+
clippy::enum_variant_names,
7+
clippy::upper_case_acronyms,
8+
clippy::wrong_self_convention
9+
)]
10+
use schemars::JsonSchema;
11+
use serde::{Deserialize, Serialize};
12+
use std::collections::{BTreeMap, BTreeSet};
13+
14+
/// Map of all known composite types.
15+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
16+
#[serde(rename_all = "camelCase")]
17+
pub struct CompositeTypes(pub BTreeMap<String, CompositeType>);
18+
19+
/// A Scalar Type.
20+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
21+
#[serde(rename_all = "camelCase")]
22+
pub struct ScalarType(pub String);
23+
24+
/// The type of values that a column, field, or argument may take.
25+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
26+
#[serde(rename_all = "camelCase")]
27+
pub enum Type {
28+
ScalarType(ScalarType),
29+
CompositeType(String),
30+
ArrayType(Box<Type>),
31+
}
32+
33+
/// Information about a composite type. These are very similar to tables, but with the crucial
34+
/// difference that composite types do not support constraints (such as NOT NULL).
35+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
36+
#[serde(rename_all = "camelCase")]
37+
pub struct CompositeType {
38+
pub name: String,
39+
pub fields: BTreeMap<String, FieldInfo>,
40+
#[serde(default)]
41+
pub description: Option<String>,
42+
}
43+
44+
/// Information about a composite type field.
45+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46+
#[serde(rename_all = "camelCase")]
47+
pub struct FieldInfo {
48+
pub name: String,
49+
pub r#type: Type,
50+
#[serde(default)]
51+
pub description: Option<String>,
52+
}
53+
54+
/// The complete list of supported binary operators for scalar types.
55+
/// Not all of these are supported for every type.
56+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
57+
#[serde(rename_all = "camelCase")]
58+
pub struct ComparisonOperators(pub BTreeMap<ScalarType, BTreeMap<String, ComparisonOperator>>);
59+
60+
/// Represents a postgres binary comparison operator
61+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
62+
#[serde(rename_all = "camelCase")]
63+
pub struct ComparisonOperator {
64+
pub operator_name: String,
65+
pub operator_kind: OperatorKind,
66+
pub argument_type: ScalarType,
67+
68+
#[serde(default = "default_true")]
69+
pub is_infix: bool,
70+
}
71+
72+
/// Is it a built-in operator, or a custom operator.
73+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74+
#[serde(rename_all = "camelCase")]
75+
pub enum OperatorKind {
76+
Equal,
77+
In,
78+
Custom,
79+
}
80+
81+
/// This is quite unfortunate: https://github.com/serde-rs/serde/issues/368
82+
/// TL;DR: we can't set default literals for serde, so if we want 'is_infix' to
83+
/// default to 'true', we have to set its default as a function that returns 'true'.
84+
fn default_true() -> bool {
85+
true
86+
}
87+
88+
/// Mapping from a "table" name to its information.
89+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
90+
#[serde(rename_all = "camelCase")]
91+
pub struct TablesInfo(pub BTreeMap<String, TableInfo>);
92+
93+
/// Information about a database table (or any other kind of relation).
94+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
95+
#[serde(rename_all = "camelCase")]
96+
pub struct TableInfo {
97+
pub schema_name: String,
98+
pub table_name: String,
99+
pub columns: BTreeMap<String, ColumnInfo>,
100+
#[serde(default)]
101+
pub uniqueness_constraints: UniquenessConstraints,
102+
#[serde(default)]
103+
pub foreign_relations: ForeignRelations,
104+
#[serde(default)]
105+
pub description: Option<String>,
106+
}
107+
108+
/// Can this column contain null values
109+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
110+
#[serde(rename_all = "camelCase")]
111+
pub enum Nullable {
112+
#[default]
113+
Nullable,
114+
NonNullable,
115+
}
116+
117+
/// Does this column have a default value.
118+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
119+
#[serde(rename_all = "camelCase")]
120+
pub enum HasDefault {
121+
#[default]
122+
NoDefault,
123+
HasDefault,
124+
}
125+
126+
/// Is this column an identity column.
127+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
128+
#[serde(rename_all = "camelCase")]
129+
pub enum IsIdentity {
130+
#[default]
131+
NotIdentity,
132+
IdentityByDefault,
133+
IdentityAlways,
134+
}
135+
136+
/// Is this column a generated column.
137+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
138+
#[serde(rename_all = "camelCase")]
139+
pub enum IsGenerated {
140+
#[default]
141+
NotGenerated,
142+
Stored,
143+
}
144+
145+
/// Information about a database column.
146+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147+
#[serde(rename_all = "camelCase")]
148+
pub struct ColumnInfo {
149+
pub name: String,
150+
pub r#type: Type,
151+
#[serde(default)]
152+
pub nullable: Nullable,
153+
#[serde(skip_serializing_if = "does_not_have_default")]
154+
#[serde(default)]
155+
pub has_default: HasDefault,
156+
#[serde(skip_serializing_if = "is_not_identity")]
157+
#[serde(default)]
158+
pub is_identity: IsIdentity,
159+
#[serde(skip_serializing_if = "is_not_generated")]
160+
#[serde(default)]
161+
pub is_generated: IsGenerated,
162+
#[serde(default)]
163+
pub description: Option<String>,
164+
}
165+
166+
fn does_not_have_default(has_default: &HasDefault) -> bool {
167+
matches!(has_default, HasDefault::NoDefault)
168+
}
169+
170+
fn is_not_identity(is_identity: &IsIdentity) -> bool {
171+
matches!(is_identity, IsIdentity::NotIdentity)
172+
}
173+
174+
fn is_not_generated(is_generated: &IsGenerated) -> bool {
175+
matches!(is_generated, IsGenerated::NotGenerated)
176+
}
177+
178+
/// A mapping from the name of a unique constraint to its value.
179+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
180+
#[serde(rename_all = "camelCase")]
181+
pub struct UniquenessConstraints(pub BTreeMap<String, UniquenessConstraint>);
182+
183+
/// The set of columns that make up a uniqueness constraint.
184+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
185+
#[serde(rename_all = "camelCase")]
186+
pub struct UniquenessConstraint(pub BTreeSet<String>);
187+
188+
/// A mapping from the name of a foreign key constraint to its value.
189+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
190+
#[serde(rename_all = "camelCase")]
191+
pub struct ForeignRelations(pub BTreeMap<String, ForeignRelation>);
192+
193+
/// A foreign key constraint.
194+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
195+
#[serde(rename_all = "camelCase")]
196+
pub struct ForeignRelation {
197+
#[serde(skip_serializing_if = "Option::is_none")]
198+
pub foreign_schema: Option<String>,
199+
pub foreign_table: String,
200+
pub column_mapping: BTreeMap<String, String>,
201+
}
202+
203+
/// All supported aggregate functions, grouped by type.
204+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
205+
#[serde(rename_all = "camelCase")]
206+
pub struct AggregateFunctions(pub BTreeMap<ScalarType, BTreeMap<String, AggregateFunction>>);
207+
208+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209+
#[serde(rename_all = "camelCase")]
210+
pub struct AggregateFunction {
211+
pub return_type: ScalarType,
212+
}
213+
214+
/// Type representation of scalar types, grouped by type.
215+
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
216+
#[serde(rename_all = "camelCase")]
217+
pub struct TypeRepresentations(pub BTreeMap<ScalarType, TypeRepresentation>);
218+
219+
/// Type representation of a scalar type.
220+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
221+
#[serde(rename_all = "camelCase")]
222+
pub enum TypeRepresentation {
223+
/// JSON booleans
224+
Boolean,
225+
/// Any JSON string
226+
String,
227+
/// float4
228+
Float32,
229+
/// float8
230+
Float64,
231+
/// int2
232+
Int16,
233+
/// int4
234+
Int32,
235+
/// int8
236+
Int64,
237+
/// numeric
238+
BigDecimal,
239+
/// timestamp
240+
Timestamp,
241+
/// timestamp with timezone
242+
Timestamptz,
243+
/// time
244+
Time,
245+
/// time with timezone
246+
Timetz,
247+
/// date
248+
Date,
249+
/// uuid
250+
UUID,
251+
/// geography
252+
Geography,
253+
/// geometry
254+
Geometry,
255+
/// Any JSON number
256+
Number,
257+
/// Any JSON number, with no decimal part
258+
Integer,
259+
/// An arbitrary json.
260+
Json,
261+
/// One of the specified string values
262+
Enum(Vec<String>),
263+
}
264+
265+
// tests
266+
267+
#[cfg(test)]
268+
mod tests {
269+
use super::{ScalarType, TypeRepresentation, TypeRepresentations};
270+
271+
#[test]
272+
fn parse_type_representations() {
273+
assert_eq!(
274+
serde_json::from_str::<TypeRepresentations>(
275+
r#"{"int4": "integer", "card_suit": {"enum": ["hearts", "clubs", "diamonds", "spades"]}}"#
276+
)
277+
.unwrap(),
278+
TypeRepresentations(
279+
[(
280+
ScalarType("int4".to_string()),
281+
TypeRepresentation::Integer
282+
), (
283+
ScalarType("card_suit".to_string()),
284+
TypeRepresentation::Enum(vec![
285+
"hearts".into(),
286+
"clubs".into(),
287+
"diamonds".into(),
288+
"spades".into()
289+
])
290+
)]
291+
.into()
292+
)
293+
);
294+
}
295+
}

0 commit comments

Comments
 (0)