Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 270 additions & 25 deletions packages/cli/src/config_parsing/entity_parsing.rs

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions packages/cli/src/config_parsing/field_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub enum Primitive {
Json,
Date,
Enum(String),
Entity(String),
}

impl Primitive {
Expand All @@ -45,7 +44,6 @@ impl Primitive {
let capitalized_enum_name = enum_name.capitalize();
format!("Enum({{config: Enums.{capitalized_enum_name}.config->Table.fromGenericEnumConfig}})")
}
Self::Entity(entity_name) => format!("Entity({{name: \"{entity_name}\"}})"),
}
}
}
Expand Down
3 changes: 0 additions & 3 deletions packages/cli/src/config_parsing/public_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -728,9 +728,6 @@ impl SystemConfig {
Primitive::Enum(name) => {
("enum".into(), Some(name.clone()), None, None, None)
}
Primitive::Entity(name) => {
("entity".into(), None, Some(name.clone()), None, None)
}
};
let db_name_for =
|backend: Option<system_config::StorageBackend>| match backend
Expand Down
77 changes: 76 additions & 1 deletion packages/cli/src/config_parsing/system_config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{
chain_helpers::get_max_reorg_depth_from_id,
entity_parsing::{Entity, GraphQLEnum, Schema},
entity_parsing::{ClickHouseEntityStorage, Entity, GqlScalar, GraphQLEnum, Schema},
env_interpolation::interpolate_config_variables,
human_config::{
self,
Expand Down Expand Up @@ -380,6 +380,11 @@ impl Storage {
}
}

/// Largest BigInt precision ClickHouse still stores as a numeric `Decimal`;
/// above this (or with no precision) it falls back to `String`. Kept in sync
/// with the BigInt branch of `getClickHouseFieldType` in ClickHouse.res.
const CLICKHOUSE_DECIMAL_MAX_PRECISION: u32 = 38;

/// Check per-entity `@storage` directives against the resolved global storage.
/// Malformed directives are raised earlier, during schema parsing.
pub fn validate_entity_storage(storage: &Storage, schema: &Schema) -> anyhow::Result<()> {
Expand Down Expand Up @@ -421,6 +426,76 @@ pub fn validate_entity_storage(storage: &Storage, schema: &Schema) -> anyhow::Re
}
}

// ClickHouse stores a BigInt whose precision is unset (or above its Decimal
// ceiling) as a String, which sorts lexicographically — wrong for anything
// in the sorting key. See the BigInt branch of `getClickHouseFieldType` in
// ClickHouse.res.
//
// Which column that applies to depends on `@storage(clickhouse: {orderBy})`:
// without it the sorting key is `id`, with it the listed fields replace `id`
// (see `makeCreateHistoryTableQuery`). Unlike the parse-time
// `validate_clickhouse_order_by_fields`, the schema is available here, so a
// relation in the sorting key can be resolved to the id it actually stores.
let bigint_stored_as_string =
|precision: Option<u32>| !precision.is_some_and(|p| p <= CLICKHOUSE_DECIMAL_MAX_PRECISION);
for entity in &entities {
let uses_clickhouse = if entity.has_storage_directive() {
entity.clickhouse.as_ref().is_some_and(|c| c.is_enabled())
} else {
clickhouse_default
};
if !uses_clickhouse {
continue;
}

let order_by = match entity.clickhouse.as_ref() {
Some(ClickHouseEntityStorage::Options(options)) => options.order_by.as_deref(),
_ => None,
};

match order_by {
Some(order_by_fields) => {
for field_name in order_by_fields {
// Existence, nullability and array-ness are already rejected
// at parse time; a miss here just means nothing to resolve.
let Some(field) = entity.get_field(field_name) else {
continue;
};
let stored =
schema.resolve_stored_scalar(&field.field_type.get_underlying_scalar())?;
if let GqlScalar::BigInt(precision) = stored {
if bigint_stored_as_string(precision) {
return Err(anyhow!(
"Invalid storage for `{}`. `clickhouse.orderBy` sorts by \
`{field_name}`, which stores a BigInt that ClickHouse keeps as a \
String (sorted lexicographically, not numerically) unless a \
precision is set. Add `@config(precision: N)` with \
N <= {CLICKHOUSE_DECIMAL_MAX_PRECISION} to the BigInt it stores \
so it sorts as a numeric Decimal.",
entity.name
));
}
}
}
}
// No custom orderBy, so `id` is the sorting key.
None => {
if let Ok(GqlScalar::BigInt(precision)) = entity.get_id_scalar() {
if bigint_stored_as_string(precision) {
return Err(anyhow!(
"Invalid storage for `{}`. Its `id` is a BigInt, which ClickHouse stores as a \
String (sorted lexicographically, not numerically) unless a precision is set. \
Since `id` is ClickHouse's sorting key, add `@config(precision: N)` with \
N <= {CLICKHOUSE_DECIMAL_MAX_PRECISION} so the id stores as a numeric Decimal, \
or set `@storage(clickhouse: {{orderBy: [...]}})` to sort by other fields.",
entity.name
));
}
}
}
}
}

let unsupported: Vec<(&str, &'static str)> = entities
.iter()
.flat_map(|e| {
Expand Down
Loading