Skip to main content

palimpsest_sql/
catalog.rs

1// Copyright 2026 Thousand Birds Inc.
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Logical catalog the parser/lower passes consult to resolve table
5//! and column references and to type-check expressions.
6
7use std::collections::BTreeMap;
8
9use crate::SqlError;
10
11/// Coarse SQL type taxonomy used during validation and lowering.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum ColumnType {
14    /// Boolean.
15    Bool,
16    /// Signed integer.
17    Int,
18    /// Floating-point.
19    Float,
20    /// UTF-8 text.
21    Text,
22    /// Timestamp (with or without timezone).
23    Timestamp,
24    /// Type couldn't be inferred yet — treat as compatible with anything.
25    Unknown,
26}
27
28impl ColumnType {
29    /// True for [`Self::Int`] and [`Self::Float`].
30    #[must_use]
31    pub const fn is_numeric(self) -> bool {
32        matches!(self, Self::Int | Self::Float)
33    }
34
35    /// Whether two column types are interchangeable in a comparison or
36    /// arithmetic context. `Unknown` is compatible with everything;
37    /// numerics promote to each other.
38    #[must_use]
39    pub fn is_compatible_with(self, other: Self) -> bool {
40        matches!((self, other), (Self::Unknown, _) | (_, Self::Unknown))
41            || self == other
42            || (self.is_numeric() && other.is_numeric())
43    }
44}
45
46/// Single column entry in a [`TableSchema`].
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ColumnSchema {
49    /// Column name (case-sensitive).
50    pub name: String,
51    /// Column type.
52    pub ty: ColumnType,
53}
54
55impl ColumnSchema {
56    /// Builds a column schema.
57    #[must_use]
58    pub fn new(name: impl Into<String>, ty: ColumnType) -> Self {
59        Self {
60            name: name.into(),
61            ty,
62        }
63    }
64}
65
66/// Schema of a single relation tracked in the [`Catalog`].
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct TableSchema {
69    /// Table name (case-sensitive).
70    pub name: String,
71    /// Ordered column list.
72    pub columns: Vec<ColumnSchema>,
73}
74
75impl TableSchema {
76    /// Builds a table schema.
77    #[must_use]
78    pub fn new(name: impl Into<String>, columns: Vec<ColumnSchema>) -> Self {
79        Self {
80            name: name.into(),
81            columns,
82        }
83    }
84
85    /// Looks a column up by name.
86    #[must_use]
87    pub fn column(&self, name: &str) -> Option<&ColumnSchema> {
88        self.columns.iter().find(|column| column.name == name)
89    }
90}
91
92/// Logical catalog: a set of [`TableSchema`]s keyed by name.
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct Catalog {
95    tables: BTreeMap<String, TableSchema>,
96}
97
98impl Catalog {
99    /// Builds a catalog from an iterator of table schemas.
100    #[must_use]
101    pub fn new(tables: impl IntoIterator<Item = TableSchema>) -> Self {
102        Self {
103            tables: tables
104                .into_iter()
105                .map(|table| (table.name.clone(), table))
106                .collect(),
107        }
108    }
109
110    /// Looks up a table by name; `None` if absent.
111    #[must_use]
112    pub fn table(&self, name: &str) -> Option<&TableSchema> {
113        self.tables.get(name)
114    }
115
116    /// Looks up a table by name, returning [`SqlError::UnknownTable`]
117    /// if absent.
118    ///
119    /// # Errors
120    /// `SqlError::UnknownTable` when the table is not in the catalog.
121    pub fn require_table(&self, name: &str) -> Result<&TableSchema, SqlError> {
122        self.table(name)
123            .ok_or_else(|| SqlError::UnknownTable(name.to_owned()))
124    }
125
126    /// Iterates over table schemas in stable (alphabetical) order.
127    pub fn tables(&self) -> impl Iterator<Item = &TableSchema> {
128        self.tables.values()
129    }
130
131    /// Built-in demo catalog used by examples and integration tests.
132    #[must_use]
133    pub fn demo() -> Self {
134        let post_columns = vec![
135            ColumnSchema::new("id", ColumnType::Int),
136            ColumnSchema::new("author_id", ColumnType::Int),
137            ColumnSchema::new("created_at", ColumnType::Timestamp),
138            ColumnSchema::new("title", ColumnType::Text),
139            ColumnSchema::new("published", ColumnType::Bool),
140        ];
141
142        Self::new([
143            TableSchema::new("posts", post_columns.clone()),
144            TableSchema::new("archived_posts", post_columns),
145            TableSchema::new(
146                "authors",
147                vec![
148                    ColumnSchema::new("id", ColumnType::Int),
149                    ColumnSchema::new("name", ColumnType::Text),
150                ],
151            ),
152            TableSchema::new(
153                "comments",
154                vec![
155                    ColumnSchema::new("id", ColumnType::Int),
156                    ColumnSchema::new("post_id", ColumnType::Int),
157                    ColumnSchema::new("author_id", ColumnType::Int),
158                    ColumnSchema::new("body", ColumnType::Text),
159                ],
160            ),
161        ])
162    }
163}