LCOV - code coverage report
Current view: top level - dashql-core/src/catalog - mod.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 97.7 % 643 628
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! The catalog publishes catalog entries (the declarations of analyzed scripts and descriptor pools)
       2              : //! atomically and resolves table names across them. Entries are immutable once published and shared
       3              : //! through [`CatalogEntryRef`]. A script entry holds only declarations and names, never the analysis,
       4              : //! so publishing does not keep ASTs or earlier analyses alive (see `docs/design/core_analysis_catalog.md`).
       5              : 
       6              : pub mod changes;
       7              : pub mod descriptor;
       8              : pub mod entry;
       9              : pub mod ids;
      10              : pub mod names;
      11              : 
      12              : pub use descriptor::DescriptorEntry;
      13              : pub use entry::*;
      14              : pub use ids::*;
      15              : pub use names::{IndexedName, NameSearchIndex};
      16              : 
      17              : use crate::error::{Error, Result};
      18              : use dashql_buffers::catalog::{
      19              :     FlatCatalogEntryT, FlatCatalogT, IndexedFlatDatabaseEntryT, IndexedFlatSchemaEntryT, IndexedFlatTableEntryT,
      20              : };
      21              : use dashql_buffers::status::StatusCode;
      22              : use std::cell::RefCell;
      23              : use crate::hash::HashMap;
      24              : use std::collections::{BTreeMap, BTreeSet};
      25              : use std::sync::atomic::{AtomicU32, Ordering};
      26              : use std::sync::Arc;
      27              : 
      28              : /// A published script: its declarations and the names that completion searches.
      29              : ///
      30              : /// The catalog keeps only this part of an analysis. Neither field refers to other entries, so
      31              : /// published scripts never keep earlier versions of each other alive.
      32              : #[derive(Clone, Debug)]
      33              : pub struct PublishedScript {
      34              :     /// The id of the catalog the script was analyzed against (`Catalog::id`)
      35              :     pub catalog_id: u64,
      36              :     /// The declarations
      37              :     pub entry: Arc<CatalogEntryData>,
      38              :     /// The names of the script
      39              :     pub names: Arc<NameSearchIndex>,
      40              : }
      41              : 
      42              : /// A published catalog entry
      43              : #[derive(Clone, Debug)]
      44              : pub enum CatalogEntryRef {
      45              :     Script(PublishedScript),
      46              :     Descriptor(Arc<DescriptorEntry>),
      47              : }
      48              : 
      49              : impl CatalogEntryRef {
      50        13400 :     pub fn data(&self) -> &CatalogEntryData {
      51        13400 :         self.shared_data()
      52        13400 :     }
      53              : 
      54              :     /// The declarations of the entry, shared with the analyzed scripts that depend on it
      55        13401 :     pub fn shared_data(&self) -> &Arc<CatalogEntryData> {
      56        13401 :         match self {
      57        13133 :             CatalogEntryRef::Script(s) => &s.entry,
      58          268 :             CatalogEntryRef::Descriptor(d) => &d.entry,
      59              :         }
      60        13401 :     }
      61              : 
      62              :     /// The name search index of the entry, built on first use
      63          110 :     pub fn name_search_index(&self) -> &NameSearchIndex {
      64          110 :         match self {
      65          106 :             CatalogEntryRef::Script(s) => &s.names,
      66            4 :             CatalogEntryRef::Descriptor(d) => d.name_search_index(),
      67              :         }
      68          110 :     }
      69              : }
      70              : 
      71              : /// A database declared by the catalog
      72              : #[derive(Clone, Debug)]
      73              : pub struct DatabaseDeclaration {
      74              :     /// The qualified object id of the database
      75              :     pub object_id: QualifiedCatalogObjectId,
      76              :     /// The database name
      77              :     pub database_name: Arc<str>,
      78              :     /// The database alias, empty when not aliased
      79              :     pub database_alias: Arc<str>,
      80              : }
      81              : 
      82              : /// A schema declared by the catalog
      83              : #[derive(Clone, Debug)]
      84              : pub struct SchemaDeclaration {
      85              :     /// The qualified object id, packs the database and schema ids
      86              :     pub object_id: QualifiedCatalogObjectId,
      87              :     /// The database name
      88              :     pub database_name: Arc<str>,
      89              :     /// The schema name
      90              :     pub schema_name: Arc<str>,
      91              : }
      92              : 
      93              : /// A catalog entry that populates a schema
      94              : #[derive(Clone, Copy, Debug)]
      95              : pub struct SchemaEntryInfo {
      96              :     /// The catalog entry
      97              :     pub catalog_entry_id: CatalogEntryId,
      98              :     /// The qualified object id of the schema
      99              :     pub catalog_schema_id: QualifiedCatalogObjectId,
     100              : }
     101              : 
     102              : /// Canonical ids, reserved during analysis and never reused
     103              : #[derive(Debug)]
     104              : struct IdReservations {
     105              :     /// The reserved database ids by database name
     106              :     database_ids_by_name: HashMap<String, CatalogDatabaseId>,
     107              :     /// The reserved schema ids by (database, schema)
     108              :     schema_ids_by_name: HashMap<(String, String), QualifiedCatalogObjectId>,
     109              :     /// The next database id to reserve
     110              :     next_database_id: CatalogDatabaseId,
     111              :     /// The next schema id to reserve
     112              :     next_schema_id: CatalogSchemaId,
     113              :     /// The reserved table and function ids of the entries, by (entry, kind, qualified name, occurrence)
     114              :     object_ids: HashMap<(CatalogEntryId, ObjectKind, QualifiedKey, u32), u32>,
     115              :     /// The next table and function id to reserve in an entry
     116              :     next_object_ids: HashMap<(CatalogEntryId, ObjectKind), u32>,
     117              :     /// The reserved column ids of the tables, by (table, column name, occurrence)
     118              :     column_ids: HashMap<(CatalogTableId, Arc<str>, u32), u32>,
     119              :     /// The next column id to reserve in a table
     120              :     next_column_ids: HashMap<CatalogTableId, u32>,
     121              : }
     122              : 
     123              : /// The kind of an object that an entry declares
     124              : #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
     125              : enum ObjectKind {
     126              :     Table,
     127              :     Function,
     128              : }
     129              : 
     130              : impl IdReservations {
     131              :     /// Reserve the id of an object of an entry
     132         1986 :     fn object_id(&mut self, entry: CatalogEntryId, kind: ObjectKind, name: &QualifiedKey, occurrence: u32) -> u32 {
     133         1986 :         let key = (entry, kind, name.clone(), occurrence);
     134         1986 :         if let Some(&id) = self.object_ids.get(&key) {
     135          407 :             return id;
     136         1579 :         }
     137         1579 :         let next = self.next_object_ids.entry((entry, kind)).or_insert(0);
     138         1579 :         let id = *next;
     139         1579 :         *next += 1;
     140         1579 :         self.object_ids.insert(key, id);
     141         1579 :         id
     142         1986 :     }
     143              : 
     144              :     /// Release the object and column ids of an entry
     145           77 :     fn release_entry(&mut self, entry: CatalogEntryId) {
     146          101 :         self.object_ids.retain(|(e, ..), _| *e != entry);
     147           82 :         self.next_object_ids.retain(|(e, _), _| *e != entry);
     148          118 :         self.column_ids.retain(|(table, ..), _| table.origin != entry);
     149           99 :         self.next_column_ids.retain(|table, _| table.origin != entry);
     150           77 :     }
     151              : }
     152              : 
     153              : /// The source of catalog entry ids.
     154              : ///
     155              : /// Catalogs that share a source never hand out the same entry id, so entry ids identify scripts and
     156              : /// descriptor pools across all of these catalogs. A catalog created with [`Catalog::new`] has a
     157              : /// source of its own.
     158              : #[derive(Clone, Debug)]
     159              : pub struct EntryIdSource {
     160              :     /// The next entry id to allocate
     161              :     next: Arc<AtomicU32>,
     162              : }
     163              : 
     164              : impl Default for EntryIdSource {
     165            0 :     fn default() -> Self {
     166            0 :         Self::new()
     167            0 :     }
     168              : }
     169              : 
     170              : impl EntryIdSource {
     171              :     /// A source that starts at the initial entry id
     172         1557 :     pub fn new() -> Self {
     173         1557 :         Self { next: Arc::new(AtomicU32::new(INITIAL_ENTRY_ID)) }
     174         1557 :     }
     175              : 
     176              :     /// Allocate an entry id
     177        13685 :     pub fn allocate(&self) -> CatalogEntryId {
     178        13685 :         let id = self.next.fetch_add(1, Ordering::Relaxed);
     179        13685 :         assert!(id != PROTO_NULL_U32, "catalog entry ID namespace exhausted");
     180        13685 :         id
     181        13685 :     }
     182              : 
     183              :     /// Was the entry id allocated by this source?
     184           57 :     pub fn allocated(&self, id: CatalogEntryId) -> bool {
     185           57 :         id >= INITIAL_ENTRY_ID && id < self.next.load(Ordering::Relaxed)
     186           57 :     }
     187              : }
     188              : 
     189              : /// Fail for a value that is not a layer, the FlatBuffers enum admits every byte
     190         1055 : fn check_layer(layer: CatalogLayer) -> Result<()> {
     191         1055 :     match layer.variant_name() {
     192         1054 :         Some(_) => Ok(()),
     193            1 :         None => Err(Error::invalid_argument("invalid catalog layer")),
     194              :     }
     195         1055 : }
     196              : 
     197              : /// The source of unique catalog ids
     198              : static NEXT_CATALOG_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
     199              : 
     200              : /// The published state of a catalog at one point in time.
     201              : ///
     202              : /// Immutable once published: analyses keep the snapshot they resolved against, so their catalog
     203              : /// locations stay valid however the catalog changes later. A snapshot holds declarations and names,
     204              : /// never analyses, so keeping one alive keeps no earlier analysis alive.
     205              : #[derive(Clone, Debug)]
     206              : pub struct CatalogSnapshot {
     207              :     /// The process-unique id of the catalog
     208              :     id: u64,
     209              :     /// The version, bumped by every modification
     210              :     version: CatalogVersion,
     211              :     /// The published entries
     212              :     entries: HashMap<CatalogEntryId, CatalogEntryRef>,
     213              :     /// The published scripts and their layers
     214              :     script_entries: HashMap<CatalogEntryId, (PublishedScript, CatalogLayer)>,
     215              :     /// The published descriptor pools
     216              :     descriptor_entries: HashMap<CatalogEntryId, Arc<DescriptorEntry>>,
     217              :     /// The entries ordered by (layer, id)
     218              :     entries_ranked: BTreeSet<(CatalogLayer, CatalogEntryId)>,
     219              :     /// The entries by (database, schema, layer, id)
     220              :     entries_by_qualified_schema: BTreeMap<(Arc<str>, Arc<str>, CatalogLayer, CatalogEntryId), SchemaEntryInfo>,
     221              :     /// The entries by (schema, layer, id)
     222              :     entries_by_schema: BTreeMap<(Arc<str>, CatalogLayer, CatalogEntryId), SchemaEntryInfo>,
     223              :     /// The databases referenced by entries
     224              :     databases: BTreeMap<Arc<str>, Arc<DatabaseDeclaration>>,
     225              :     /// The schemas referenced by entries
     226              :     schemas: BTreeMap<(Arc<str>, Arc<str>), Arc<SchemaDeclaration>>,
     227              : }
     228              : 
     229              : /// The catalog: the current snapshot and the ids that outlive snapshots.
     230              : ///
     231              : /// Reads go to the current snapshot (`Deref`). A modification copies the snapshot if an analysis
     232              : /// still holds it (`DerefMut` is `Arc::make_mut`), so published snapshots never change.
     233              : #[derive(Debug)]
     234              : pub struct Catalog {
     235              :     /// The current snapshot
     236              :     current: Arc<CatalogSnapshot>,
     237              :     /// The canonical ids, reserved during analysis and never reused
     238              :     ids: RefCell<IdReservations>,
     239              :     /// The source of the entry ids
     240              :     entry_ids: EntryIdSource,
     241              : }
     242              : 
     243              : impl std::ops::Deref for Catalog {
     244              :     type Target = CatalogSnapshot;
     245              : 
     246        22747 :     fn deref(&self) -> &CatalogSnapshot {
     247        22747 :         &self.current
     248        22747 :     }
     249              : }
     250              : 
     251              : impl std::ops::DerefMut for Catalog {
     252         7943 :     fn deref_mut(&mut self) -> &mut CatalogSnapshot {
     253         7943 :         Arc::make_mut(&mut self.current)
     254         7943 :     }
     255              : }
     256              : 
     257              : impl Default for Catalog {
     258           18 :     fn default() -> Self {
     259           18 :         Self::new()
     260           18 :     }
     261              : }
     262              : 
     263              : impl Catalog {
     264         1556 :     pub fn new() -> Self {
     265         1556 :         Self::with_entry_ids(EntryIdSource::new())
     266         1556 :     }
     267              : 
     268              :     /// A catalog that allocates its entry ids from a shared source
     269         1558 :     pub fn with_entry_ids(entry_ids: EntryIdSource) -> Self {
     270         1558 :         Self {
     271         1558 :             current: Arc::new(CatalogSnapshot {
     272         1558 :                 id: NEXT_CATALOG_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
     273         1558 :                 version: 1,
     274         1558 :                 entries: HashMap::default(),
     275         1558 :                 script_entries: HashMap::default(),
     276         1558 :                 descriptor_entries: HashMap::default(),
     277         1558 :                 entries_ranked: BTreeSet::new(),
     278         1558 :                 entries_by_qualified_schema: BTreeMap::new(),
     279         1558 :                 entries_by_schema: BTreeMap::new(),
     280         1558 :                 databases: BTreeMap::new(),
     281         1558 :                 schemas: BTreeMap::new(),
     282         1558 :             }),
     283         1558 :             ids: RefCell::new(IdReservations {
     284         1558 :                 database_ids_by_name: HashMap::default(),
     285         1558 :                 schema_ids_by_name: HashMap::default(),
     286         1558 :                 next_database_id: INITIAL_DATABASE_ID,
     287         1558 :                 next_schema_id: INITIAL_SCHEMA_ID,
     288         1558 :                 object_ids: HashMap::default(),
     289         1558 :                 next_object_ids: HashMap::default(),
     290         1558 :                 column_ids: HashMap::default(),
     291         1558 :                 next_column_ids: HashMap::default(),
     292         1558 :             }),
     293         1558 :             entry_ids,
     294         1558 :         }
     295         1558 :     }
     296              : 
     297              :     /// The current snapshot
     298         3734 :     pub fn snapshot(&self) -> &Arc<CatalogSnapshot> {
     299         3734 :         &self.current
     300         3734 :     }
     301              : 
     302              :     /// Is a snapshot of this catalog current? A snapshot stays current while the version stays,
     303              :     /// i.e. while the declarations that analyses resolve against stay the same.
     304         1064 :     pub fn is_current(&self, snapshot: &CatalogSnapshot) -> bool {
     305         1064 :         snapshot.id == self.current.id && snapshot.version == self.current.version
     306         1064 :     }
     307              : }
     308              : 
     309              : impl CatalogSnapshot {
     310              :     /// The process-unique id of the catalog
     311         3674 :     pub fn id(&self) -> u64 {
     312         3674 :         self.id
     313         3674 :     }
     314              : 
     315         5228 :     pub fn version(&self) -> CatalogVersion {
     316         5228 :         self.version
     317         5228 :     }
     318              : 
     319           30 :     pub fn contains(&self, id: CatalogEntryId) -> bool {
     320           30 :         self.entries.contains_key(&id)
     321           30 :     }
     322              : 
     323          858 :     pub fn entry(&self, id: CatalogEntryId) -> Option<&CatalogEntryRef> {
     324          858 :         self.entries.get(&id)
     325          858 :     }
     326              : 
     327              :     /// The layer of an entry
     328            5 :     pub fn layer(&self, id: CatalogEntryId) -> Option<CatalogLayer> {
     329            5 :         match self.script_entries.get(&id) {
     330            1 :             Some((_, layer)) => Some(*layer),
     331            4 :             None => self.descriptor_entries.get(&id).map(|d| d.layer),
     332              :         }
     333            5 :     }
     334              : 
     335              :     /// The entries in resolution order: by layer, then by entry id
     336         1145 :     pub fn entries_ranked(&self) -> impl Iterator<Item = (CatalogLayer, &CatalogEntryRef)> {
     337         1150 :         self.entries_ranked.iter().map(|(layer, id)| (*layer, &self.entries[id]))
     338         1145 :     }
     339              : 
     340            8 :     pub fn databases(&self) -> &BTreeMap<Arc<str>, Arc<DatabaseDeclaration>> {
     341            8 :         &self.databases
     342            8 :     }
     343              : 
     344           46 :     pub fn schemas(&self) -> &BTreeMap<(Arc<str>, Arc<str>), Arc<SchemaDeclaration>> {
     345           46 :         &self.schemas
     346           46 :     }
     347              : 
     348              :     /// The entries by (database, schema, layer, id)
     349           20 :     pub fn entries_by_qualified_schema(&self) -> &BTreeMap<(Arc<str>, Arc<str>, CatalogLayer, CatalogEntryId), SchemaEntryInfo> {
     350           20 :         &self.entries_by_qualified_schema
     351           20 :     }
     352              : 
     353              :     /// The entries by (schema, layer, id)
     354           39 :     pub fn entries_by_schema(&self) -> &BTreeMap<(Arc<str>, CatalogLayer, CatalogEntryId), SchemaEntryInfo> {
     355           39 :         &self.entries_by_schema
     356           39 :     }
     357              : }
     358              : 
     359              : impl Catalog {
     360              :     /// Allocate an entry id
     361        13685 :     pub fn allocate_entry_id(&self) -> CatalogEntryId {
     362        13685 :         self.entry_ids.allocate()
     363        13685 :     }
     364              : 
     365              :     /// Was the entry id allocated by the id source of the catalog?
     366           57 :     pub fn entry_id_allocated(&self, id: CatalogEntryId) -> bool {
     367           57 :         self.entry_ids.allocated(id)
     368           57 :     }
     369              : 
     370              :     /// Reserve the canonical id of a database
     371         1299 :     pub fn reserve_database_id(&self, database: &str) -> QualifiedCatalogObjectId {
     372         1299 :         let mut ids = self.ids.borrow_mut();
     373         1299 :         if let Some(&id) = ids.database_ids_by_name.get(database) {
     374          480 :             return QualifiedCatalogObjectId::database(id);
     375          819 :         }
     376          819 :         let id = ids.next_database_id;
     377          819 :         ids.next_database_id += 1;
     378          819 :         ids.database_ids_by_name.insert(database.to_string(), id);
     379          819 :         QualifiedCatalogObjectId::database(id)
     380         1299 :     }
     381              : 
     382              :     /// Reserve the id of a table of an entry.
     383              :     ///
     384              :     /// The id is keyed by the qualified name and the occurrence of that name among the tables of the
     385              :     /// entry, so a table keeps its id across versions of the entry however its declarations move. A
     386              :     /// fresh entry gets the ids in declaration order. Dropping the entry releases its ids.
     387         1952 :     pub fn reserve_table_id(&self, entry: CatalogEntryId, name: &QualifiedKey, occurrence: u32) -> CatalogTableId {
     388         1952 :         CatalogTableId::new(entry, self.ids.borrow_mut().object_id(entry, ObjectKind::Table, name, occurrence))
     389         1952 :     }
     390              : 
     391              :     /// Reserve the id of a function of an entry, like [`Catalog::reserve_table_id`]
     392           34 :     pub fn reserve_function_id(&self, entry: CatalogEntryId, name: &QualifiedKey, occurrence: u32) -> CatalogFunctionId {
     393           34 :         CatalogFunctionId::new(entry, self.ids.borrow_mut().object_id(entry, ObjectKind::Function, name, occurrence))
     394           34 :     }
     395              : 
     396              :     /// Reserve the id of a column of a table, keyed by the column name and its occurrence among the
     397              :     /// columns of the table
     398         4261 :     pub fn reserve_column_id(&self, table: CatalogTableId, name: &Arc<str>, occurrence: u32) -> u32 {
     399         4261 :         let mut ids = self.ids.borrow_mut();
     400         4261 :         let key = (table, name.clone(), occurrence);
     401         4261 :         if let Some(&id) = ids.column_ids.get(&key) {
     402          385 :             return id;
     403         3876 :         }
     404         3876 :         let next = ids.next_column_ids.entry(table).or_insert(0);
     405         3876 :         let id = *next;
     406         3876 :         *next += 1;
     407         3876 :         ids.column_ids.insert(key, id);
     408         3876 :         id
     409         4261 :     }
     410              : 
     411              :     /// Reserve the canonical id of a schema
     412         1321 :     pub fn reserve_schema_id(&self, database: &str, schema: &str, db_id: QualifiedCatalogObjectId) -> QualifiedCatalogObjectId {
     413         1321 :         let mut ids = self.ids.borrow_mut();
     414         1321 :         let key = (database.to_string(), schema.to_string());
     415         1321 :         if let Some(&id) = ids.schema_ids_by_name.get(&key) {
     416          475 :             return id;
     417          846 :         }
     418          846 :         let schema_id = ids.next_schema_id;
     419          846 :         ids.next_schema_id += 1;
     420          846 :         let id = QualifiedCatalogObjectId::schema(db_id.unpack_database_id(), schema_id);
     421          846 :         ids.schema_ids_by_name.insert(key, id);
     422          846 :         id
     423         1321 :     }
     424              : 
     425         1004 :     fn check_canonical_ids(&self, entry: &CatalogEntryData) -> Result<()> {
     426         1004 :         let ids = self.ids.borrow();
     427         1021 :         for (name, &i) in &entry.databases_by_name {
     428         1019 :             let object_id = entry.database_references[i].object_id;
     429         1019 :             match ids.database_ids_by_name.get(&**name) {
     430         1019 :                 Some(&id) if object_id == QualifiedCatalogObjectId::database(id) => {}
     431            0 :                 _ => return Err(Error::status(StatusCode::CATALOG_ID_OUT_OF_SYNC)),
     432              :             }
     433              :         }
     434         1031 :         for ((db, schema), &i) in &entry.schemas_by_qualified_name {
     435         1029 :             let object_id = entry.schema_references[i].object_id;
     436         1029 :             match ids.schema_ids_by_name.get(&(db.to_string(), schema.to_string())) {
     437         1029 :                 Some(&id) if id == object_id => {}
     438            0 :                 _ => return Err(Error::status(StatusCode::CATALOG_ID_OUT_OF_SYNC)),
     439              :             }
     440              :         }
     441         1004 :         Ok(())
     442         1004 :     }
     443              : 
     444              :     /// Load an analyzed script
     445          854 :     pub fn load_script(&mut self, script: impl Into<PublishedScript>, layer: CatalogLayer) -> Result<()> {
     446          854 :         self.load_scripts(&[(script.into(), layer)])
     447          854 :     }
     448              : 
     449              :     /// Atomically add or replace scripts
     450         1384 :     pub fn load_scripts<S: Clone + Into<PublishedScript>>(&mut self, scripts: &[(S, CatalogLayer)]) -> Result<()> {
     451         1384 :         if scripts.is_empty() {
     452          410 :             return Ok(());
     453          974 :         }
     454          974 :         let scripts: Vec<(PublishedScript, CatalogLayer)> =
     455         1006 :             scripts.iter().map(|(script, layer)| (script.clone().into(), *layer)).collect();
     456          974 :         let mut batch_ids = BTreeSet::new();
     457         1006 :         for (_, layer) in &scripts {
     458         1006 :             check_layer(*layer)?;
     459              :         }
     460         1006 :         for (script, _) in &scripts {
     461         1006 :             if script.catalog_id != self.id {
     462            1 :                 return Err(Error::status(StatusCode::CATALOG_MISMATCH));
     463         1005 :             }
     464         1005 :             let id = script.entry.catalog_entry_id;
     465         1005 :             if !batch_ids.insert(id) {
     466            1 :                 return Err(Error::status(StatusCode::EXTERNAL_ID_COLLISION));
     467         1004 :             }
     468         1004 :             if self.entries.contains_key(&id) && !self.script_entries.contains_key(&id) {
     469            0 :                 return Err(Error::status(StatusCode::EXTERNAL_ID_COLLISION));
     470         1004 :             }
     471         1004 :             self.check_canonical_ids(&script.entry)?;
     472              :         }
     473              :         // Scripts that declare what their published versions declare replace them in place: the
     474              :         // indexes stay valid and the catalog version stays, so the analyses of other scripts remain
     475              :         // current
     476          972 :         let unchanged = scripts.iter().all(|(script, layer)| {
     477          972 :             self.script_entries.get(&script.entry.catalog_entry_id).is_some_and(|(published, published_layer)| {
     478          216 :                 published_layer == layer && published.entry.same_declarations(&script.entry)
     479          216 :             })
     480          972 :         });
     481          972 :         if unchanged {
     482          204 :             for (script, layer) in &scripts {
     483          204 :                 let id = script.entry.catalog_entry_id;
     484          204 :                 self.script_entries.insert(id, (script.clone(), *layer));
     485          204 :                 self.entries.insert(id, CatalogEntryRef::Script(script.clone()));
     486          204 :             }
     487          204 :             return Ok(());
     488          768 :         }
     489          768 :         let mut staged = self.script_entries.clone();
     490          798 :         for (script, layer) in &scripts {
     491          798 :             staged.insert(script.entry.catalog_entry_id, (script.clone(), *layer));
     492          798 :         }
     493          768 :         let descriptors = self.descriptor_entries.clone();
     494          768 :         self.publish_entries(staged, descriptors)
     495         1384 :     }
     496              : 
     497              :     /// Build all indexes before committing
     498          816 :     fn publish_entries(
     499          816 :         &mut self,
     500          816 :         script_entries: HashMap<CatalogEntryId, (PublishedScript, CatalogLayer)>,
     501          816 :         descriptor_entries: HashMap<CatalogEntryId, Arc<DescriptorEntry>>,
     502          816 :     ) -> Result<()> {
     503          816 :         let mut entries = HashMap::default();
     504          816 :         let mut entries_ranked = BTreeSet::new();
     505          816 :         let mut entries_by_qualified_schema = BTreeMap::new();
     506          816 :         let mut entries_by_schema = BTreeMap::new();
     507          816 :         let mut databases: BTreeMap<Arc<str>, Arc<DatabaseDeclaration>> = BTreeMap::new();
     508          816 :         let mut schemas: BTreeMap<(Arc<str>, Arc<str>), Arc<SchemaDeclaration>> = BTreeMap::new();
     509              : 
     510          900 :         let mut stage = |entry: CatalogEntryRef, layer: CatalogLayer| -> Result<()> {
     511          897 :             let data = entry.data();
     512          897 :             let entry_id = data.catalog_entry_id;
     513          897 :             entries_ranked.insert((layer, entry_id));
     514          915 :             for (name, &i) in &data.databases_by_name {
     515          913 :                 let db = &data.database_references[i];
     516          913 :                 if let Some(existing) = databases.get(name) {
     517           81 :                     if existing.object_id != db.object_id {
     518            0 :                         return Err(Error::status(StatusCode::CATALOG_ID_OUT_OF_SYNC));
     519           81 :                     }
     520           81 :                     continue;
     521          832 :                 }
     522          832 :                 databases.insert(
     523          832 :                     db.database_name.clone(),
     524          832 :                     Arc::new(DatabaseDeclaration {
     525          832 :                         object_id: db.object_id,
     526          832 :                         database_name: db.database_name.clone(),
     527          832 :                         database_alias: db.database_alias.clone(),
     528          832 :                     }),
     529              :                 );
     530              :             }
     531          935 :             for ((db_name, schema_name), &i) in &data.schemas_by_qualified_name {
     532          933 :                 let schema = &data.schema_references[i];
     533          933 :                 let key = (db_name.clone(), schema_name.clone());
     534          933 :                 if let Some(existing) = schemas.get(&key) {
     535           76 :                     if existing.object_id != schema.object_id {
     536            0 :                         return Err(Error::status(StatusCode::CATALOG_ID_OUT_OF_SYNC));
     537           76 :                     }
     538           76 :                     continue;
     539          857 :                 }
     540          857 :                 if !databases.contains_key(db_name) {
     541            0 :                     return Err(Error::status(StatusCode::CATALOG_ID_OUT_OF_SYNC));
     542          857 :                 }
     543          857 :                 schemas.insert(
     544          857 :                     key,
     545          857 :                     Arc::new(SchemaDeclaration {
     546          857 :                         object_id: schema.object_id,
     547          857 :                         database_name: db_name.clone(),
     548          857 :                         schema_name: schema.schema_name.clone(),
     549          857 :                     }),
     550              :                 );
     551              :             }
     552          935 :             for ((db_name, schema_name), &i) in &data.schemas_by_qualified_name {
     553          933 :                 let info = SchemaEntryInfo {
     554          933 :                     catalog_entry_id: entry_id,
     555          933 :                     catalog_schema_id: data.schema_references[i].object_id,
     556          933 :                 };
     557          933 :                 entries_by_qualified_schema.insert((db_name.clone(), schema_name.clone(), layer, entry_id), info);
     558          933 :                 entries_by_schema.insert((schema_name.clone(), layer, entry_id), info);
     559          933 :             }
     560          897 :             if entries.insert(entry_id, entry.clone()).is_some() {
     561            0 :                 return Err(Error::status(StatusCode::EXTERNAL_ID_COLLISION));
     562          897 :             }
     563          897 :             Ok(())
     564          897 :         };
     565          893 :         for (script, layer) in script_entries.values() {
     566          846 :             stage(CatalogEntryRef::Script(script.clone()), *layer)?;
     567              :         }
     568          818 :         for descriptor in descriptor_entries.values() {
     569           51 :             stage(CatalogEntryRef::Descriptor(descriptor.clone()), descriptor.layer)?;
     570              :         }
     571          816 :         self.script_entries = script_entries;
     572          816 :         self.descriptor_entries = descriptor_entries;
     573          816 :         self.entries = entries;
     574          816 :         self.entries_ranked = entries_ranked;
     575          816 :         self.entries_by_qualified_schema = entries_by_qualified_schema;
     576          816 :         self.entries_by_schema = entries_by_schema;
     577          816 :         self.databases = databases;
     578          816 :         self.schemas = schemas;
     579          816 :         self.version += 1;
     580          816 :         Ok(())
     581          816 :     }
     582              : 
     583              :     /// Replace a descriptor pool atomically
     584           54 :     pub fn replace_descriptor(&mut self, id: CatalogEntryId, layer: CatalogLayer, descriptor: &[u8]) -> Result<()> {
     585           54 :         if !self.entry_id_allocated(id)
     586           51 :             || (self.entries.contains_key(&id) && !self.descriptor_entries.contains_key(&id))
     587              :         {
     588            5 :             return Err(Error::invalid_argument("invalid or colliding receiver catalog entry ID"));
     589           49 :         }
     590           49 :         check_layer(layer)?;
     591              :         // Identical bytes declare the same, the catalog version stays and the analyses stay current
     592           48 :         if self.descriptor_entries.get(&id).is_some_and(|e| e.layer == layer && e.bytes == descriptor) {
     593            1 :             return Ok(());
     594           47 :         }
     595           47 :         let entry = DescriptorEntry::new(self, id, layer, descriptor)?;
     596           44 :         let mut staged = self.descriptor_entries.clone();
     597           44 :         staged.insert(id, Arc::new(entry));
     598           44 :         let scripts = self.script_entries.clone();
     599           44 :         self.publish_entries(scripts, staged)
     600           54 :     }
     601              : 
     602              :     /// Drop a descriptor pool
     603            8 :     pub fn drop_descriptor(&mut self, id: CatalogEntryId) -> Result<()> {
     604            8 :         if !self.descriptor_entries.contains_key(&id) {
     605            4 :             return Ok(());
     606            4 :         }
     607            4 :         self.ids.borrow_mut().release_entry(id);
     608            4 :         let mut staged = self.descriptor_entries.clone();
     609            4 :         staged.remove(&id);
     610            4 :         let scripts = self.script_entries.clone();
     611            4 :         self.publish_entries(scripts, staged)
     612            8 :     }
     613              : 
     614              :     /// Drop a script
     615           74 :     pub fn drop_script(&mut self, id: CatalogEntryId) {
     616              :         // Also for scripts that were analyzed but never published. A descriptor pool published under the id of the
     617              :         // script owns the ids, they stay until the pool is dropped.
     618           74 :         if !self.descriptor_entries.contains_key(&id) {
     619           73 :             self.ids.borrow_mut().release_entry(id);
     620           74 :         }
     621           74 :         let Some((analyzed, layer)) = self.script_entries.remove(&id) else {
     622           59 :             return;
     623              :         };
     624           16 :         for (db_name, schema_name) in analyzed.entry.schemas_by_qualified_name.keys() {
     625           14 :             self.entries_by_qualified_schema.remove(&(db_name.clone(), schema_name.clone(), layer, id));
     626           14 :             self.entries_by_schema.remove(&(schema_name.clone(), layer, id));
     627           14 :         }
     628           15 :         self.entries_ranked.remove(&(layer, id));
     629           15 :         self.entries.remove(&id);
     630           16 :         for (db_name, schema_name) in analyzed.entry.schemas_by_qualified_name.keys() {
     631           14 :             let lower = (db_name.clone(), schema_name.clone(), MIN_LAYER, 0);
     632           14 :             let remaining = self.entries_by_qualified_schema.range(lower..).next();
     633            1 :             match remaining {
     634            1 :                 Some(((d, s, _, _), _)) if d == db_name && s == schema_name => {}
     635           13 :                 _ => {
     636           13 :                     self.schemas.remove(&(db_name.clone(), schema_name.clone()));
     637           13 :                 }
     638              :             }
     639              :         }
     640           16 :         for name in analyzed.entry.databases_by_name.keys() {
     641           14 :             let referenced = self.entries.values().any(|e| e.data().databases_by_name.contains_key(name));
     642           14 :             if !referenced {
     643           13 :                 self.databases.remove(name);
     644           13 :             }
     645              :         }
     646           15 :         self.version += 1;
     647           74 :     }
     648              : 
     649              :     /// Clear the catalog
     650            2 :     pub fn clear(&mut self) {
     651            2 :         self.entries_by_qualified_schema.clear();
     652            2 :         self.entries_by_schema.clear();
     653            2 :         self.entries_ranked.clear();
     654            2 :         self.entries.clear();
     655            2 :         self.script_entries.clear();
     656            2 :         self.descriptor_entries.clear();
     657            2 :         self.schemas.clear();
     658            2 :         self.databases.clear();
     659            2 :         self.version += 1;
     660            2 :     }
     661              : 
     662              : }
     663              : 
     664              : impl CatalogSnapshot {
     665              :     /// Flatten the catalog.
     666              :     ///
     667              :     /// Databases, schemas and tables are merged across entries and ordered by name. A table name that
     668              :     /// occurs in several entries is taken from the first entry in resolution order. Names go through a
     669              :     /// dictionary, built in resolution order, so that JS only decodes the names it renders.
     670            9 :     pub fn flatten(&self) -> FlatCatalogT {
     671              :         /// A table of the flattened catalog
     672              :         struct TableNode {
     673              :             /// The table id
     674              :             table_id: CatalogTableId,
     675              :             /// The index of the table name in the name dictionary
     676              :             name_id: usize,
     677              :             /// The index of the first column in the column nodes
     678              :             columns_begin: usize,
     679              :             /// The number of columns
     680              :             column_count: usize,
     681              :         }
     682              :         /// A schema of the flattened catalog
     683              :         struct SchemaNode {
     684              :             /// The schema id
     685              :             schema_id: CatalogSchemaId,
     686              :             /// The index of the schema name in the name dictionary
     687              :             name_id: usize,
     688              :             /// The table node indexes by table name
     689              :             children: BTreeMap<Arc<str>, usize>,
     690              :         }
     691              :         /// A database of the flattened catalog
     692              :         struct DatabaseNode {
     693              :             /// The database id
     694              :             database_id: CatalogDatabaseId,
     695              :             /// The index of the database name in the name dictionary
     696              :             name_id: usize,
     697              :             /// The schema node indexes by schema name
     698              :             children: BTreeMap<Arc<str>, usize>,
     699              :         }
     700              : 
     701            9 :         let mut name_dictionary: Vec<String> = Vec::new();
     702            9 :         let mut name_dictionary_index: HashMap<Arc<str>, usize> = HashMap::default();
     703          105 :         let mut add_name = |name: &Arc<str>| -> usize {
     704          105 :             *name_dictionary_index.entry(name.clone()).or_insert_with(|| {
     705           98 :                 name_dictionary.push(name.to_string());
     706           98 :                 name_dictionary.len() - 1
     707           98 :             })
     708          105 :         };
     709              : 
     710            9 :         let mut database_nodes: Vec<DatabaseNode> = Vec::new();
     711            9 :         let mut schema_nodes: Vec<SchemaNode> = Vec::new();
     712            9 :         let mut table_nodes: Vec<TableNode> = Vec::new();
     713              :         // (column id, name id)
     714            9 :         let mut column_nodes: Vec<(u32, usize)> = Vec::new();
     715            9 :         let mut root: BTreeMap<Arc<str>, usize> = BTreeMap::new();
     716            9 :         let mut database_node_map: HashMap<QualifiedCatalogObjectId, usize> = HashMap::default();
     717            9 :         let mut schema_node_map: HashMap<QualifiedCatalogObjectId, usize> = HashMap::default();
     718              : 
     719              :         // Visit entries in resolution order, the name dictionary must not depend on the hash implementation
     720           11 :         for (_, entry) in self.entries_ranked() {
     721            9 :             let data = entry.data();
     722           11 :             for db_ref in &data.database_references {
     723           11 :                 if database_node_map.contains_key(&db_ref.object_id) {
     724            3 :                     continue;
     725            8 :                 }
     726            8 :                 let name_id = add_name(&db_ref.database_name);
     727            8 :                 database_nodes.push(DatabaseNode {
     728            8 :                     database_id: db_ref.database_id(),
     729            8 :                     name_id,
     730            8 :                     children: BTreeMap::new(),
     731            8 :                 });
     732            8 :                 database_node_map.insert(db_ref.object_id, database_nodes.len() - 1);
     733            8 :                 root.entry(db_ref.database_name.clone()).or_insert(database_nodes.len() - 1);
     734              :             }
     735           11 :             for &i in data.schemas_by_qualified_name.values() {
     736           11 :                 let schema_ref = &data.schema_references[i];
     737           11 :                 if schema_node_map.contains_key(&schema_ref.object_id) {
     738            3 :                     continue;
     739            8 :                 }
     740            8 :                 let name_id = add_name(&schema_ref.schema_name);
     741            8 :                 schema_nodes.push(SchemaNode {
     742            8 :                     schema_id: schema_ref.schema_id(),
     743            8 :                     name_id,
     744            8 :                     children: BTreeMap::new(),
     745            8 :                 });
     746            8 :                 let schema_node = schema_nodes.len() - 1;
     747            8 :                 schema_node_map.insert(schema_ref.object_id, schema_node);
     748            8 :                 let db_node = database_node_map[&QualifiedCatalogObjectId::database(schema_ref.database_id())];
     749            8 :                 database_nodes[db_node].children.entry(schema_ref.schema_name.clone()).or_insert(schema_node);
     750              :             }
     751              :         }
     752              : 
     753              :         // Tables are not deduplicated among entries, the first entry in resolution order wins
     754           11 :         for (_, entry) in self.entries_ranked() {
     755           18 :             for table in &entry.data().table_declarations {
     756           18 :                 let schema_node = schema_node_map[&table.catalog_schema_id];
     757           18 :                 let table_name = &table.table_name.table_name.text;
     758           18 :                 if schema_nodes[schema_node].children.contains_key(table_name) {
     759            0 :                     continue;
     760           18 :                 }
     761           18 :                 let columns_begin = column_nodes.len();
     762           71 :                 for column in &table.table_columns {
     763           71 :                     let name_id = add_name(&column.column_name.text);
     764           71 :                     column_nodes.push((column.column_id(), name_id));
     765           71 :                 }
     766           18 :                 let name_id = add_name(table_name);
     767           18 :                 table_nodes.push(TableNode {
     768           18 :                     table_id: table.table_id(),
     769           18 :                     name_id,
     770           18 :                     columns_begin,
     771           18 :                     column_count: table.table_columns.len(),
     772           18 :                 });
     773           18 :                 schema_nodes[schema_node].children.insert(table_name.clone(), table_nodes.len() - 1);
     774              :             }
     775              :         }
     776              : 
     777              :         // (entry index, parent index, object id, name id, child begin, child count)
     778          105 :         let entry = |idx: usize, parent: usize, object_id: u64, name_id: usize, begin: usize, count: usize| {
     779          105 :             FlatCatalogEntryT {
     780          105 :                 flat_entry_idx: idx as u32,
     781          105 :                 flat_parent_idx: parent as u32,
     782          105 :                 catalog_object_id: object_id,
     783          105 :                 name_id: name_id as u32,
     784          105 :                 child_begin: begin as u32,
     785          105 :                 child_count: count as u32,
     786          105 :             }
     787          105 :         };
     788            9 :         let mut databases = Vec::new();
     789            9 :         let mut schemas = Vec::new();
     790            9 :         let mut tables = Vec::new();
     791            9 :         let mut columns = Vec::new();
     792            9 :         let mut databases_by_id = Vec::new();
     793            9 :         let mut schemas_by_id = Vec::new();
     794            9 :         let mut tables_by_id = Vec::new();
     795           10 :         for &db_node in root.values() {
     796            8 :             let db = &database_nodes[db_node];
     797            8 :             let db_idx = databases.len();
     798            8 :             databases.push(entry(db_idx, 0, db.database_id as u64, db.name_id, schemas.len(), db.children.len()));
     799            8 :             databases_by_id.push(IndexedFlatDatabaseEntryT {
     800            8 :                 database_id: db.database_id,
     801            8 :                 flat_entry_idx: db_idx as u32,
     802            8 :             });
     803            8 :             for &schema_node in db.children.values() {
     804            8 :                 let schema = &schema_nodes[schema_node];
     805            8 :                 let schema_idx = schemas.len();
     806            8 :                 let schema_id = schema.schema_id as u64;
     807            8 :                 schemas.push(entry(schema_idx, db_idx, schema_id, schema.name_id, tables.len(), schema.children.len()));
     808            8 :                 schemas_by_id.push(IndexedFlatSchemaEntryT {
     809            8 :                     schema_id: schema.schema_id,
     810            8 :                     flat_entry_idx: schema_idx as u32,
     811            8 :                 });
     812           18 :                 for &table_node in schema.children.values() {
     813           18 :                     let table = &table_nodes[table_node];
     814           18 :                     let table_idx = tables.len();
     815           18 :                     let table_id = table.table_id.pack();
     816           18 :                     let column_count = table.column_count;
     817           18 :                     tables.push(entry(table_idx, schema_idx, table_id, table.name_id, columns.len(), column_count));
     818           18 :                     tables_by_id.push(IndexedFlatTableEntryT {
     819           18 :                         table_id,
     820           18 :                         flat_entry_idx: table_idx as u32,
     821           18 :                     });
     822           18 :                     let table_columns = table.columns_begin..table.columns_begin + table.column_count;
     823           71 :                     for &(column_id, name_id) in &column_nodes[table_columns] {
     824           71 :                         columns.push(entry(columns.len(), table_idx, column_id as u64, name_id, 0, 0));
     825           71 :                     }
     826              :                 }
     827              :             }
     828              :         }
     829            9 :         databases_by_id.sort_by_key(|e| e.database_id);
     830            9 :         schemas_by_id.sort_by_key(|e| e.schema_id);
     831            9 :         tables_by_id.sort_by_key(|e| e.table_id);
     832              : 
     833            9 :         let mut out = FlatCatalogT::default();
     834            9 :         out.catalog_version = self.version as u64;
     835            9 :         out.name_dictionary = Some(name_dictionary);
     836            9 :         out.databases = Some(databases);
     837            9 :         out.schemas = Some(schemas);
     838            9 :         out.tables = Some(tables);
     839            9 :         out.columns = Some(columns);
     840            9 :         out.databases_by_id = Some(databases_by_id);
     841            9 :         out.schemas_by_id = Some(schemas_by_id);
     842            9 :         out.tables_by_id = Some(tables_by_id);
     843            9 :         out
     844            9 :     }
     845              : 
     846              :     /// A table by id
     847          195 :     pub fn table_by_id(&self, table_id: CatalogTableId) -> Option<&TableDeclaration> {
     848          195 :         self.entries.get(&table_id.origin)?.data().table_by_id(table_id)
     849          195 :     }
     850              : 
     851              :     /// A table by location
     852         7648 :     pub fn table(&self, loc: TableLoc) -> Option<&TableDeclaration> {
     853         7648 :         self.entries.get(&loc.entry)?.data().table_declarations.get(loc.index as usize)
     854         7648 :     }
     855              : 
     856              :     /// Resolve a table across all entries except `ignore_entry`
     857         2206 :     pub fn resolve_table(&self, name: &QualifiedTableName, ignore_entry: CatalogEntryId, out: &mut Vec<TableLoc>, limit: usize) {
     858         2206 :         let db = name.database_name.text.clone();
     859         2206 :         let schema = name.schema_name.text.clone();
     860              :         // Direct lookups of the qualified name
     861         2206 :         let lower = (db.clone(), schema.clone(), MIN_LAYER, 0);
     862         2206 :         let upper = (db.clone(), schema.clone(), MAX_LAYER, CatalogEntryId::MAX);
     863         2206 :         let key = name.key();
     864         2457 :         for ((_, _, _, candidate), _) in self.entries_by_qualified_schema.range(lower..=upper) {
     865         2237 :             if *candidate == ignore_entry {
     866          249 :                 continue;
     867         1988 :             }
     868         1988 :             let entry = self.entries[candidate].data();
     869         1988 :             if let Some(&i) = entry.tables_by_qualified_name.get(&key) {
     870         1789 :                 out.push(entry.table_loc(i));
     871         1789 :                 if out.len() >= limit {
     872            0 :                     break;
     873         1789 :                 }
     874          199 :             }
     875              :         }
     876              :         // A direct hit always wins
     877         2206 :         if !out.is_empty() {
     878         1783 :             return;
     879          423 :         }
     880          423 :         if !name.database_name.text.is_empty() {
     881           46 :             return;
     882          377 :         }
     883          377 :         if !name.schema_name.text.is_empty() {
     884              :             // Table and schema name: search the entries that populate the schema in any database
     885           42 :             let lower = (schema.clone(), MIN_LAYER, 0);
     886           42 :             let upper = (schema.clone(), MAX_LAYER, CatalogEntryId::MAX);
     887           42 :             for ((schema_name, _, candidate), _) in self.entries_by_schema.range(lower..=upper) {
     888            5 :                 if *candidate == ignore_entry {
     889            0 :                     continue;
     890            5 :                 }
     891            5 :                 self.entries[candidate].data().resolve_table_in_schema(schema_name, &name.table_name.text, out, limit);
     892            5 :                 if out.len() >= limit {
     893            0 :                     break;
     894            5 :                 }
     895              :             }
     896              :         } else {
     897              :             // Only the table name: search all entries in resolution order
     898          336 :             for (_, entry_id) in &self.entries_ranked {
     899          133 :                 if *entry_id == ignore_entry {
     900            1 :                     continue;
     901          132 :                 }
     902          132 :                 self.entries[entry_id].data().resolve_table_everywhere(&name.table_name.text, out, limit);
     903          132 :                 if out.len() >= limit {
     904            0 :                     break;
     905          132 :                 }
     906              :             }
     907              :         }
     908         2206 :     }
     909              : }
     910              : 
     911              : /// Describe a catalog entry of a layer
     912            7 : fn describe_entry(layer: CatalogLayer, entry: &CatalogEntryRef) -> dashql_buffers::catalog::CatalogEntryT {
     913              :     use dashql_buffers::catalog::{CatalogEntryT, CatalogEntryType, SchemaDescriptorT, SchemaTableColumnT, SchemaTableT};
     914            7 :     let data = entry.data();
     915            7 :     let mut out = CatalogEntryT::default();
     916            7 :     out.catalog_entry_id = data.catalog_entry_id;
     917            7 :     out.catalog_entry_type = CatalogEntryType::DESCRIPTOR_POOL;
     918            7 :     out.layer = layer;
     919            7 :     match entry {
     920              :         // Scripts are described as one anonymous schema with all tables
     921              :         CatalogEntryRef::Script(_) => {
     922            3 :             let tables = data
     923            3 :                 .table_declarations
     924            3 :                 .iter()
     925            3 :                 .enumerate()
     926            4 :                 .map(|(i, table)| {
     927            4 :                     let mut t = SchemaTableT::default();
     928            4 :                     t.table_id = i as u32;
     929            4 :                     t.table_name = Some(table.table_name.table_name.text.to_string());
     930            4 :                     t.columns = Some(
     931            4 :                         table
     932            4 :                             .table_columns
     933            4 :                             .iter()
     934            5 :                             .map(|c| {
     935            5 :                                 let mut column = SchemaTableColumnT::default();
     936            5 :                                 column.column_name = Some(c.column_name.text.to_string());
     937            5 :                                 column
     938            5 :                             })
     939            4 :                             .collect(),
     940              :                     );
     941            4 :                     t
     942            4 :                 })
     943            3 :                 .collect();
     944            3 :             let mut schema = SchemaDescriptorT::default();
     945            3 :             schema.tables = Some(tables);
     946            3 :             out.schemas = Some(vec![schema]);
     947              :         }
     948              :         CatalogEntryRef::Descriptor(_) => {
     949            4 :             let schemas = data
     950            4 :                 .schemas_by_qualified_name
     951            4 :                 .iter()
     952            7 :                 .map(|((db, name), &i)| {
     953            7 :                     let schema_id = data.schema_references[i].object_id;
     954            7 :                     let tables = data
     955            7 :                         .table_declarations
     956            7 :                         .iter()
     957           17 :                         .filter(|t| t.catalog_schema_id == schema_id)
     958            9 :                         .map(|table| {
     959            9 :                             let mut t = SchemaTableT::default();
     960            9 :                             t.table_id = table.table_id().object;
     961            9 :                             t.table_name = Some(table.table_name.table_name.text.to_string());
     962            9 :                             t.columns = Some(
     963            9 :                                 table
     964            9 :                                     .table_columns
     965            9 :                                     .iter()
     966            9 :                                     .enumerate()
     967           12 :                                     .map(|(i, c)| {
     968           12 :                                         let mut column = SchemaTableColumnT::default();
     969           12 :                                         column.column_name = Some(c.column_name.text.to_string());
     970           12 :                                         column.ordinal_position = i as u32;
     971           12 :                                         column
     972           12 :                                     })
     973            9 :                                     .collect(),
     974              :                             );
     975            9 :                             t
     976            9 :                         })
     977            7 :                         .collect();
     978            7 :                     let mut schema = SchemaDescriptorT::default();
     979            7 :                     schema.database_name = Some(db.to_string());
     980            7 :                     schema.schema_name = Some(name.to_string());
     981            7 :                     schema.tables = Some(tables);
     982            7 :                     schema
     983            7 :                 })
     984            4 :                 .collect();
     985            4 :             out.schemas = Some(schemas);
     986              :         }
     987              :     }
     988            7 :     out
     989            7 : }
     990              : 
     991              : impl CatalogSnapshot {
     992              :     /// Describe all entries in resolution order
     993            2 :     pub fn describe_entries(&self) -> dashql_buffers::catalog::CatalogEntriesT {
     994            2 :         let mut out = dashql_buffers::catalog::CatalogEntriesT::default();
     995            2 :         out.entries = Some(self.entries_ranked().map(|(layer, e)| describe_entry(layer, e)).collect());
     996            2 :         out
     997            2 :     }
     998              : 
     999              :     /// Describe one entry, None if the entry is unknown
    1000            6 :     pub fn describe_entries_of(&self, id: CatalogEntryId) -> Option<dashql_buffers::catalog::CatalogEntriesT> {
    1001            6 :         let entry = self.entries.get(&id)?;
    1002            5 :         let layer = self.layer(id)?;
    1003            5 :         let mut out = dashql_buffers::catalog::CatalogEntriesT::default();
    1004            5 :         out.entries = Some(vec![describe_entry(layer, entry)]);
    1005            5 :         Some(out)
    1006            6 :     }
    1007              : 
    1008              :     /// The statistics of the catalog content
    1009            3 :     pub fn statistics(&self) -> dashql_buffers::catalog::CatalogStatisticsT {
    1010            3 :         let mut content = dashql_buffers::catalog::CatalogContentStatisticsT::default();
    1011            3 :         content.database_count = self.databases.len() as u32;
    1012            3 :         content.schema_count = self.schemas.len() as u32;
    1013            3 :         content.table_count = self.entries.values().map(|e| e.data().table_declarations.len() as u32).sum();
    1014            3 :         content.table_column_count = self
    1015            3 :             .entries
    1016            3 :             .values()
    1017            3 :             .flat_map(|e| e.data().table_declarations.iter())
    1018            5 :             .map(|t| t.table_columns.len() as u32)
    1019            3 :             .sum();
    1020            3 :         let mut out = dashql_buffers::catalog::CatalogStatisticsT::default();
    1021            3 :         out.content = Some(content);
    1022            3 :         out
    1023            3 :     }
    1024              : }
        

Generated by: LCOV version 2.0-1