Skip to main content

nodedb_sql/types/
collection.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Collection and column metadata types for query planning.
4
5use super::query::EngineType;
6use crate::types_expr::SqlDataType;
7
8/// Metadata about a collection for query planning.
9#[derive(Debug, Clone)]
10pub struct CollectionInfo {
11    pub name: String,
12    pub engine: EngineType,
13    pub columns: Vec<ColumnInfo>,
14    pub primary_key: Option<String>,
15    pub has_auto_tier: bool,
16    /// Secondary indexes available for planner rewrites. Populated by the
17    /// catalog adapter from `StoredCollection.indexes`. `Building` entries
18    /// are included so the planner can see them but MUST be skipped when
19    /// choosing an index lookup — only `Ready` indexes back query rewrites.
20    pub indexes: Vec<IndexSpec>,
21    /// When `true`, this collection stores every write as an immutable
22    /// version keyed by `system_from_ms`. Enables `FOR SYSTEM_TIME AS OF`
23    /// and `FOR VALID_TIME` queries. Only meaningful for document engines
24    /// today; other engines ignore this flag.
25    pub bitemporal: bool,
26    /// Primary engine hint from the catalog.
27    pub primary: nodedb_types::PrimaryEngine,
28    /// Vector-primary configuration. `Some` only when
29    /// `primary == PrimaryEngine::Vector`.
30    pub vector_primary: Option<nodedb_types::VectorPrimaryConfig>,
31    /// How this collection's rows are distributed across vShards.
32    ///
33    /// Authoritative per-collection partition metadata. Future routing layers
34    /// read this instead of inferring distribution from engine type.
35    pub partition_strategy: nodedb_types::PartitionStrategy,
36    /// Whether a name outside `columns` resolves in this relation.
37    ///
38    /// A stored collection derives it from its engine. A synthesized relation
39    /// sets it independently: a derived, LATERAL, or CTE alias is closed when
40    /// its projection list is inferable, open only when it is `*` over an open
41    /// source. `pg_catalog` relations are open because NodeDB models a subset
42    /// of the columns clients ask for.
43    pub open_schema: bool,
44}
45
46impl CollectionInfo {
47    /// The openness a stored collection on `engine` carries.
48    pub fn open_schema_for(engine: EngineType) -> bool {
49        crate::engine_rules::resolve_engine_rules(engine).accepts_undeclared_columns()
50    }
51}
52
53/// Secondary index metadata surfaced to the SQL planner.
54#[derive(Debug, Clone)]
55pub struct IndexSpec {
56    pub name: String,
57    /// Canonical field path (`$.email`, `$.user.name`, or plain column name
58    /// for strict documents — the catalog layer stores them uniformly).
59    pub field: String,
60    pub unique: bool,
61    pub case_insensitive: bool,
62    /// Build state. Only `Ready` indexes drive query rewrites.
63    pub state: IndexState,
64    /// Partial-index predicate as raw SQL text (`WHERE <expr>` body
65    /// without the keyword), or `None` for full indexes. The planner
66    /// uses this to reject rewrites whose WHERE clause doesn't entail
67    /// the predicate — matching against such a partial index would
68    /// omit rows the index didn't cover.
69    pub predicate: Option<String>,
70}
71
72/// Planner-facing index state. Mirrors the catalog variant but lives here
73/// so the SQL crate doesn't depend on `nodedb` internals.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum IndexState {
76    Building,
77    Ready,
78}
79
80/// Metadata about a single column.
81#[derive(Debug, Clone)]
82pub struct ColumnInfo {
83    pub name: String,
84    pub data_type: SqlDataType,
85    pub nullable: bool,
86    pub is_primary_key: bool,
87    /// Default value expression (e.g. "UUID_V7", "ULID", "NANOID(10)", "0", "'active'").
88    pub default: Option<String>,
89    /// Raw type string as stored in the catalog (e.g. `"JSON"`, `"TEXT"`, `"FLOAT64"`).
90    /// `None` for columns synthesized by the planner (e.g. auto-injected `id`).
91    /// Columnar INSERT converters use this to reconstruct the exact `ColumnType`
92    /// so JSON / Geometry / UUID columns are not incorrectly inferred as String.
93    /// Write coercion reads a `VECTOR(dim)` declaration from it on a column
94    /// whose `data_type` advertises text.
95    pub raw_type: Option<String>,
96    /// Declared width of an integer column, resolved once from the catalog at
97    /// adapter-construction time. `None` for non-integer columns and for
98    /// integer columns whose declared type carried no width.
99    ///
100    /// This is deliberately a resolved [`IntWidth`] rather than another raw
101    /// string: it is read on both the write path (range validation) and the
102    /// read path (`RowDescription` OID and binary payload width), and those
103    /// two must agree exactly. Resolving once at the catalog boundary is what
104    /// makes disagreement unrepresentable.
105    pub int_width: Option<nodedb_types::columnar::IntWidth>,
106    /// Declared width of a floating-point column, resolved once from the
107    /// catalog at adapter-construction time. `None` for non-float columns and
108    /// for float columns whose declared type the catalog never recorded.
109    ///
110    /// The float analogue of [`ColumnInfo::int_width`], and resolved from the
111    /// same source. Read on the read path to select the `RowDescription` OID
112    /// (700 vs 701) and the binary payload width (4 vs 8 bytes), and read on
113    /// the write path by `check_declared_float_ranges` — but only for the
114    /// narrower failure mode a float has: narrowing an `f64` to `f32` rounds
115    /// rather than wraps (PostgreSQL accepts-and-rounds a `double` literal
116    /// into a `real` column, and so does nodedb), so only a finite value
117    /// that would overflow to infinity is rejected, not the full out-of-range
118    /// check `int_width` gets.
119    pub float_width: Option<nodedb_types::columnar::FloatWidth>,
120}