LCOV - code coverage report
Current view: top level - dashql-core-session/src - planner.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 97.5 % 945 921
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! The planner.
       2              : //!
       3              : //! Plans relation-level hazards between the statements of several scripts: every statement becomes
       4              : //! a node with the relations it reads, writes or defines, and edges order conflicting accesses.
       5              : //! Opaque SELECT functions may have side effects; barriers order statements but do not promise
       6              : //! backend session affinity.
       7              : //!
       8              : //! Scripts run on connections, every connection has its own catalog. Relations of different connections
       9              : //! never conflict. A script references the result of another script with a table parameter, `:foo` reads
      10              : //! the terminal query of `foo.sql` (see `script_references`). A reference within one connection inlines the
      11              : //! query into the statement as a common table expression, a reference of the LOCAL connection to another
      12              : //! connection scans the result as Parquet file that the host materializes, a reference of a REMOTE
      13              : //! connection to another connection is an error.
      14              : //!
      15              : //! A plan covers the whole session or the target scripts and what they need: the statements that an inlined
      16              : //! reference needs before it, and the scripts that a scan reference scans unless the host provides
      17              : //! their results. A plan of the session leaves out the scripts whose results the host provides. A plan of targets treats statements like a run of a single script: statements of every
      18              : //! kind keep their SQL, and the statements of a script run in their order.
      19              : 
      20              : use crate::compiler::{compile_visualization, produces_output, read_executable_text, read_executable_text_with};
      21              : use crate::script_references::{self, ReferenceErrors, ReferenceKind, ScriptReferences};
      22              : use crate::session_inputs::{bind_inputs, collect_session_inputs, describe_call, SessionInputs};
      23              : use dashql_buffers::analyzer::AnalyzerErrorSeverity;
      24              : use dashql_buffers::formatting::FormattingDialect;
      25              : use dashql_buffers::execution::{
      26              :     ExecutionGraphAccessMode as Mode, ExecutionGraphAccessT, ExecutionGraphCatalogObjectIDT, ExecutionGraphConnectionKind,
      27              :     ExecutionGraphConnectionT, ExecutionGraphDiagnosticT, ExecutionGraphEdgeReason as Reason, ExecutionGraphEdgeT, ExecutionGraphEntityT,
      28              :     ExecutionGraphAttachmentT, ExecutionGraphEntityType, ExecutionGraphMaterializationT, ExecutionGraphNodeT, ExecutionGraphScriptReferenceT, ExecutionGraphScriptT,
      29              :     ExecutionGraphT, ExecutionGraphUnresolvedReferenceT,
      30              : };
      31              : use dashql_buffers::parser::{AttributeKey as Key, Node, NodeType, StatementType as Type, TempType, TextSpanT};
      32              : use dashql_core::analyzer::{AnalyzedScript, ReferencedTable, RelationExpression, TableRole};
      33              : use dashql_core::catalog::{Catalog, CatalogEntryId, CatalogEntryRef, QualifiedCatalogObjectId, QualifiedTableName, TableDeclaration};
      34              : use dashql_core::pack;
      35              : use dashql_core::script::Script;
      36              : use dashql_parser::attrs;
      37              : use dashql_parser::parser::context::resolve_text_span;
      38              : use dashql_parser::parser::ParsedScript;
      39              : use dashql_parser::scanner::ScannedScript;
      40              : use dashql_parser::span::TextSpan;
      41              : use std::collections::{BTreeMap, BTreeSet};
      42              : use std::sync::Arc;
      43              : 
      44              : /// A relation name: (database, schema, table)
      45              : type Name = (String, String, String);
      46              : 
      47              : const NONE: u32 = u32::MAX;
      48              : 
      49         2588 : fn resource_name(name: &QualifiedTableName) -> Name {
      50         2588 :     (name.database_name.text.to_string(), name.schema_name.text.to_string(), name.table_name.text.to_string())
      51         2588 : }
      52              : 
      53              : /// The table declarations of an entry with their AST statement ids
      54         1079 : fn tables(entry: &CatalogEntryRef) -> &[TableDeclaration] {
      55         1079 :     &entry.data().table_declarations
      56         1079 : }
      57              : 
      58              : /// The kind of a connection
      59              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      60              : pub enum ConnectionKind {
      61              :     /// A database that only reads its own relations
      62              :     Remote,
      63              :     /// The embedded Hyper, which scans the materialized results of scripts of other connections
      64              :     Local,
      65              : }
      66              : 
      67              : /// A connection of the plan
      68              : #[derive(Clone, Copy)]
      69              : pub struct PlannerConnection<'a> {
      70              :     /// The connection id of the host
      71              :     pub id: &'a str,
      72              :     /// The kind of the connection
      73              :     pub kind: ConnectionKind,
      74              :     /// The catalog of the connection, its scripts were analyzed with it
      75              :     pub catalog: &'a Catalog,
      76              :     /// The SQL dialect of the connection, the planner prints the values of inputs in it
      77              :     pub dialect: FormattingDialect,
      78              :     /// The database of relations without a database name, `create table t` and `db.schema.t` are one relation
      79              :     pub default_database: &'a str,
      80              :     /// The schema of relations without a schema name
      81              :     pub default_schema: &'a str,
      82              : }
      83              : 
      84              : /// A script of the plan
      85              : #[derive(Clone, Copy)]
      86              : pub struct PlannerScript<'a> {
      87              :     /// The script
      88              :     pub script: &'a Script,
      89              :     /// The name of the script, `foo` for `foo.sql`, that table parameters reference
      90              :     pub name: &'a str,
      91              :     /// The connection index
      92              :     pub connection: u32,
      93              :     /// The name of the Parquet file of the result of the script below the mount prefix, if a statement of another
      94              :     /// connection scans it. `s<index>.parquet` if absent.
      95              :     pub materialization_file: Option<&'a str>,
      96              : }
      97              : 
      98              : /// An attachment of the session, a Parquet file that the table parameter `:name` reads
      99              : #[derive(Clone, Copy)]
     100              : pub struct PlannerAttachment<'a> {
     101              :     /// The name, `foo` for `foo.parquet`
     102              :     pub name: &'a str,
     103              :     /// The file name below the attachment prefix
     104              :     pub file: &'a str,
     105              :     /// Changes when the file changes, e.g. its size and modification time. The signature of a script that reads the
     106              :     /// attachment covers it.
     107              :     pub stamp: &'a str,
     108              : }
     109              : 
     110              : /// What a plan covers
     111              : #[derive(Clone, Copy)]
     112              : pub enum PlanScope<'a> {
     113              :     /// Every statement of every script whose result the host doesn't provide, in session order
     114              :     Session,
     115              :     /// The target scripts and what they need
     116              :     Targets(&'a [u32]),
     117              :     /// Every statement of every script whose analysis is current with its text, for the dataflow of a session. Like a
     118              :     /// plan of targets, but problems are diagnostics that don't stop the plan: a script without a current analysis is
     119              :     /// left out, as are script references with errors or to scripts that were left out. An analysis of an earlier
     120              :     /// catalog revision is planned as it is. A plan that left something out is not valid.
     121              :     Tolerant,
     122              : }
     123              : 
     124              : /// A plan request
     125              : #[derive(Clone, Copy)]
     126              : pub struct SessionPlanRequest<'a> {
     127              :     /// The connections
     128              :     pub connections: &'a [PlannerConnection<'a>],
     129              :     /// The scripts in session order
     130              :     pub scripts: &'a [PlannerScript<'a>],
     131              :     /// What the plan covers
     132              :     pub scope: PlanScope<'a>,
     133              :     /// The scripts whose materialized results the host provides. A plan computes them only for its targets, a scan
     134              :     /// reads the file of the result.
     135              :     pub materialized: &'a [u32],
     136              :     /// The directory that the host mounts materialized results in, e.g. the query result cache at `/mnt/external/cache`
     137              :     pub mount_prefix: &'a str,
     138              :     /// The attachments of the session
     139              :     pub attachments: &'a [PlannerAttachment<'a>],
     140              :     /// The directory that the host mounts the attachments in, e.g. `/mnt/external/attachments/<session>`
     141              :     pub attachment_prefix: &'a str,
     142              : }
     143              : 
     144              : /// A script with a current analysis
     145              : pub(crate) struct Prepared<'a> {
     146              :     pub(crate) scanned: &'a ScannedScript,
     147              :     pub(crate) parsed: &'a ParsedScript,
     148              :     pub(crate) analyzed: &'a Arc<AnalyzedScript>,
     149              : }
     150              : 
     151              : impl<'a> Prepared<'a> {
     152              :     /// The analysis of a script if it is current with the text, whatever catalog it was analyzed with. Script
     153              :     /// references depend on the text only.
     154         3620 :     pub(crate) fn lenient(script: &'a Script) -> Option<Self> {
     155         3620 :         script.current_analysis().map(|(scanned, parsed, analyzed)| Prepared { scanned, parsed, analyzed })
     156         3620 :     }
     157              : 
     158              :     /// Does the script parse without errors?
     159         1614 :     pub(crate) fn parses(&self) -> bool {
     160         1614 :         self.scanned.errors.is_empty() && self.parsed.errors.is_empty()
     161         1614 :     }
     162              : 
     163              :     /// The last statement
     164         3296 :     pub(crate) fn terminal(&self) -> Option<u32> {
     165         3296 :         self.parsed.statements.len().checked_sub(1).map(|s| s as u32)
     166         3296 :     }
     167              : }
     168              : 
     169              : /// How much of a script a plan includes
     170              : #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
     171              : enum Include {
     172              :     /// Nothing
     173              :     None,
     174              :     /// No statement, the single query of the script is inlined into its consumers, its references are planned
     175              :     Inlined,
     176              :     /// Every statement
     177              :     All,
     178              : }
     179              : 
     180              : /// The state of the plan
     181              : struct PlanState {
     182              :     /// The execution graph, completed with the lists below when finished
     183              :     graph: ExecutionGraphT,
     184              :     /// The entity indexes by connection and table object id
     185              :     entities: BTreeMap<(u32, QualifiedCatalogObjectId), u32>,
     186              :     /// The unresolved reference indexes by connection and name
     187              :     unresolved_references: BTreeMap<(u32, Name), u32>,
     188              :     /// The interned table entities, in interning order
     189              :     entity_list: Vec<ExecutionGraphEntityT>,
     190              :     /// The interned unresolved references, in interning order
     191              :     unresolved_list: Vec<ExecutionGraphUnresolvedReferenceT>,
     192              :     /// The diagnostics of the plan
     193              :     diagnostics: Vec<ExecutionGraphDiagnosticT>,
     194              :     /// Do errors leave something out of the plan instead of stopping it? See `PlanScope::Tolerant`.
     195              :     tolerant: bool,
     196              :     /// Did a tolerant plan leave something out?
     197              :     partial: bool,
     198              :     /// The inputs of the session, the scalar parameters that the statements bind
     199              :     inputs: SessionInputs,
     200              :     /// The text span of the statement of every node
     201              :     spans: Vec<TextSpan>,
     202              :     /// The relations that a node reads only through the queries of inlined scripts: (node, entity, unresolved)
     203              :     inherited: BTreeSet<(u32, u32, u32)>,
     204              : }
     205              : 
     206              : /// A plan and what the planner derived on the way, the dataflow of a session projects it
     207              : pub(crate) struct Plan {
     208              :     /// The execution graph
     209              :     pub(crate) graph: ExecutionGraphT,
     210              :     /// The script references, an invalid one keeps the script that its name finds
     211              :     pub(crate) references: ScriptReferences,
     212              :     /// The inputs of the session
     213              :     pub(crate) inputs: SessionInputs,
     214              :     /// The text span of the statement of every node
     215              :     pub(crate) spans: Vec<TextSpan>,
     216              :     /// The relations that a node reads only through the queries of inlined scripts: (node, entity, unresolved)
     217              :     pub(crate) inherited: BTreeSet<(u32, u32, u32)>,
     218              : }
     219              : 
     220              : impl PlanState {
     221              :     /// Report a diagnostic, an error invalidates the plan
     222          301 :     fn report(&mut self, node: u32, script: u32, text_span: Option<TextSpan>, message: String, error: bool) {
     223          301 :         let mut d = ExecutionGraphDiagnosticT::default();
     224          301 :         d.node = node;
     225          301 :         d.script = script;
     226          301 :         d.message = opt(Some(message));
     227          301 :         d.text_span = text_span.map(|s| {
     228          110 :             let mut t = TextSpanT::default();
     229          110 :             t.offset = s.offset;
     230          110 :             t.length = s.length;
     231          110 :             t
     232          110 :         });
     233          301 :         d.error = error;
     234          301 :         self.diagnostics.push(d);
     235          301 :         if error && self.tolerant {
     236           13 :             self.partial = true;
     237          292 :         } else if error {
     238          150 :             self.graph.valid = false;
     239          155 :         }
     240          301 :     }
     241              : 
     242              :     /// Report a diagnostic of a statement. It is a warning in every scope: the statement runs as written, a barrier
     243              :     /// orders it against the other statements of its connection.
     244          135 :     fn diagnose(&mut self, node: u32, script: u32, message: String) {
     245          135 :         self.report(node, script, None, message, false);
     246          135 :     }
     247              : 
     248          924 :     fn intern_entity(&mut self, connection: u32, table: &TableDeclaration) -> u32 {
     249          924 :         if let Some(&i) = self.entities.get(&(connection, table.object_id)) {
     250          469 :             return i;
     251          455 :         }
     252          455 :         let i = self.entity_list.len() as u32;
     253          455 :         self.entities.insert((connection, table.object_id), i);
     254          455 :         let mut entity = ExecutionGraphEntityT::default();
     255          455 :         let mut id = ExecutionGraphCatalogObjectIDT::default();
     256          455 :         id.object_type = ExecutionGraphEntityType::TABLE;
     257          455 :         id.object_id = table.table_id().pack();
     258          455 :         id.sub_object_id = 0;
     259          455 :         entity.catalog_object_id = id;
     260          455 :         (entity.catalog_database_id, entity.catalog_schema_id) = table.catalog_schema_id.unpack_schema_id();
     261          455 :         let (db, schema, name) = resource_name(&table.table_name);
     262          455 :         entity.database_name = opt(Some(db));
     263          455 :         entity.schema_name = opt(Some(schema));
     264          455 :         entity.name = opt(Some(name));
     265          455 :         entity.connection = connection;
     266          455 :         self.entity_list.push(entity);
     267          455 :         i
     268          924 :     }
     269              : 
     270           41 :     fn intern_unresolved(&mut self, connection: u32, name: &Name) -> u32 {
     271           41 :         if let Some(&i) = self.unresolved_references.get(&(connection, name.clone())) {
     272            9 :             return i;
     273           32 :         }
     274           32 :         let i = self.unresolved_list.len() as u32;
     275           32 :         self.unresolved_references.insert((connection, name.clone()), i);
     276           32 :         let mut r = ExecutionGraphUnresolvedReferenceT::default();
     277           32 :         r.database_name = opt(Some(name.0.clone()));
     278           32 :         r.schema_name = opt(Some(name.1.clone()));
     279           32 :         r.name = opt(Some(name.2.clone()));
     280           32 :         r.connection = connection;
     281           32 :         self.unresolved_list.push(r);
     282           32 :         i
     283           41 :     }
     284              : }
     285              : 
     286              : /// The relations of a connection: the declarations of its catalog and of the planned scripts, and the live
     287              : /// namespace while the statements are planned in order
     288              : #[derive(Default)]
     289              : struct ConnectionState<'a> {
     290              :     /// The node of the statement that declares a table, by packed table id
     291              :     producers: BTreeMap<u64, u32>,
     292              :     /// The declarations by object id
     293              :     declarations: BTreeMap<QualifiedCatalogObjectId, &'a TableDeclaration>,
     294              :     /// The kinds of the published declarations
     295              :     published_views: BTreeMap<QualifiedCatalogObjectId, bool>,
     296              :     /// Declarations that are a view in one place and a table in another
     297              :     incompatible_kinds: BTreeSet<QualifiedCatalogObjectId>,
     298              :     /// The views by packed table id
     299              :     views: BTreeSet<u64>,
     300              :     /// The relations that a planned statement created
     301              :     live_producers: BTreeSet<Name>,
     302              :     /// The relations that a planned statement dropped
     303              :     dropped_resources: BTreeSet<Name>,
     304              :     /// The views that a planned statement created
     305              :     live_views: BTreeSet<Name>,
     306              :     /// The declarations by name, as the planned statements see them
     307              :     live_namespace: BTreeMap<Name, &'a TableDeclaration>,
     308              : }
     309              : 
     310              : impl<'a> ConnectionState<'a> {
     311              :     /// The name of a relation and the declaration that it reads, None if it is unresolved or its declaration is
     312              :     /// incompatible or unavailable. Analysis can bind an earlier access to a later declaration of the same name, the
     313              :     /// live lifetime still owns that access.
     314          770 :     fn resolve(&self, relation: &RelationExpression) -> (Name, Option<&'a TableDeclaration>) {
     315          770 :         let name = resource_name(relation.resolved_table.as_ref().map(|t| &t.table_name).unwrap_or(&relation.table_name));
     316          770 :         let Some(resolved) = &relation.resolved_table else { return (name, None) };
     317          735 :         let declaration = self.declarations.get(&resolved.catalog_table_id).copied().filter(|d| {
     318          735 :             d.catalog_schema_id == resolved.catalog_schema_id && resource_name(&d.table_name) == name && !self.incompatible_kinds.contains(&resolved.catalog_table_id)
     319          735 :         });
     320          735 :         let owned = self.live_producers.contains(&name) || self.producers.contains_key(&resolved.catalog_table_id.unpack_table_id().pack());
     321          735 :         let table = declaration.map(|d| self.live_namespace.get(&name).copied().filter(|_| owned).unwrap_or(d));
     322          735 :         (name, table)
     323          770 :     }
     324              : }
     325              : 
     326              : /// Validate the analysis of a script against the catalog of its connection
     327          995 : fn prepare<'a>(state: &mut PlanState, index: u32, script: &'a Script, catalog: &Catalog) -> Option<Prepared<'a>> {
     328          995 :     let Some(prepared) = Prepared::lenient(script) else {
     329           12 :         state.report(NONE, index, None, "missing or stale script analysis".into(), true);
     330           12 :         return None;
     331              :     };
     332          983 :     let analyzed = prepared.analyzed;
     333          983 :     if analyzed.snapshot.id() != catalog.id() {
     334            1 :         state.report(NONE, index, None, "script was analyzed with another catalog than the one of its connection".into(), true);
     335            1 :         return None;
     336          982 :     }
     337              :     // A tolerant plan takes the analysis of an earlier catalog revision, its declarations keep their ids
     338          982 :     if !catalog.is_current(&analyzed.snapshot) && !state.tolerant {
     339            1 :         state.report(NONE, index, None, "stale catalog analysis".into(), true);
     340            1 :         return None;
     341          981 :     }
     342          981 :     Some(prepared)
     343          995 : }
     344              : 
     345              : /// Report the syntax errors of a script, they invalidate a plan of the session
     346          767 : fn diagnose_syntax(state: &mut PlanState, index: u32, prepared: &Prepared) {
     347          767 :     let (scanned, parsed) = (prepared.scanned, prepared.parsed);
     348          767 :     for e in &scanned.errors {
     349            3 :         state.report(NONE, index, None, format!("scanner: {}", e.message), true);
     350            3 :     }
     351          767 :     for e in &parsed.errors {
     352            9 :         state.report(NONE, index, None, format!("parser: {}", e.message), true);
     353            9 :     }
     354          767 :     if parsed.statements.is_empty() {
     355           18 :         let text = scanned.text.as_bytes();
     356           18 :         let mut offset = 0;
     357           30 :         while offset < text.len() {
     358           24 :             let comment = scanned.comments.iter().find(|c| c.offset as usize <= offset && offset < (c.offset + c.length) as usize);
     359           24 :             if let Some(c) = comment {
     360            9 :                 offset = (c.offset + c.length) as usize;
     361            9 :                 continue;
     362           15 :             }
     363           15 :             if !b" \t\r\n\x0c;".contains(&text[offset]) {
     364           12 :                 state.report(NONE, index, None, "unsupported SQL: parser produced no statements".into(), true);
     365           12 :                 break;
     366            3 :             }
     367            3 :             offset += 1;
     368              :         }
     369          749 :     }
     370          767 : }
     371              : 
     372              : /// Does a script run only on request? Its current analysis has a `@manual` tag, or it calls a client function that
     373              : /// writes: a run of the session and of input changes would send the write again with every run.
     374         1658 : pub(crate) fn runs_on_request(script: &Script) -> bool {
     375         1658 :     script.current_analysis().is_some_and(|(_, _, analyzed)| analyzed.manual || analyzed.calls_writes())
     376         1658 : }
     377              : 
     378              : /// Does a script have a `@manual` tag?
     379          109 : pub(crate) fn has_manual_tag(script: &Script) -> bool {
     380          109 :     script.current_analysis().is_some_and(|(_, _, analyzed)| analyzed.manual)
     381          109 : }
     382              : 
     383              : /// The scripts whose statements a plan includes in full before their references: the targets, or the scripts of the
     384              : /// session whose results the host doesn't provide and that don't run only on request
     385         1032 : fn seeds(request: &SessionPlanRequest) -> Vec<u32> {
     386         1032 :     match request.scope {
     387          820 :         PlanScope::Session => (0..request.scripts.len() as u32)
     388         1560 :             .filter(|&i| !request.materialized.contains(&i) && !runs_on_request(request.scripts[i as usize].script))
     389          820 :             .collect(),
     390          116 :         PlanScope::Targets(targets) => targets.to_vec(),
     391           96 :         PlanScope::Tolerant => (0..request.scripts.len() as u32).collect(),
     392              :     }
     393         1032 : }
     394              : 
     395              : /// Plan the statements of the scripts of a session. Consumes the current analysis without analyzing, publishing or
     396              : /// changing scripts.
     397          475 : pub fn plan_session(request: &SessionPlanRequest) -> ExecutionGraphT {
     398          475 :     plan(request).graph
     399          475 : }
     400              : 
     401              : /// Plan the statements of the scripts of a session, see `plan_session`
     402          516 : pub(crate) fn plan(request: &SessionPlanRequest) -> Plan {
     403              :     // A plan of targets, and a tolerant plan like one, prepares what its targets need, runs the statements of a script
     404              :     // in order and passes a script that doesn't parse through as written
     405          516 :     let targeted = matches!(request.scope, PlanScope::Targets(_) | PlanScope::Tolerant);
     406          516 :     let tolerant = matches!(request.scope, PlanScope::Tolerant);
     407          516 :     let mut state = PlanState {
     408          516 :         graph: ExecutionGraphT::default(),
     409          516 :         entities: BTreeMap::new(),
     410          516 :         unresolved_references: BTreeMap::new(),
     411          516 :         entity_list: Vec::new(),
     412          516 :         unresolved_list: Vec::new(),
     413          516 :         diagnostics: Vec::new(),
     414          516 :         tolerant,
     415          516 :         partial: false,
     416          516 :         inputs: collect_session_inputs(request),
     417          516 :         spans: Vec::new(),
     418          516 :         inherited: BTreeSet::new(),
     419          516 :     };
     420          516 :     state.graph.valid = true;
     421          516 :     let script_count = request.scripts.len();
     422              : 
     423              :     // The request itself
     424          516 :     let mut catalogs: BTreeSet<u64> = BTreeSet::new();
     425          592 :     for connection in request.connections {
     426          592 :         if !catalogs.insert(connection.catalog.id()) {
     427            1 :             state.report(NONE, NONE, None, format!("connections share a catalog: {}", connection.id), true);
     428          591 :         }
     429              :     }
     430              :     // Catalog entry ids are unique within a catalog
     431          516 :     let mut script_ids: BTreeSet<(u32, CatalogEntryId)> = BTreeSet::new();
     432          516 :     let mut names: BTreeSet<&str> = BTreeSet::new();
     433         1018 :     for (i, s) in request.scripts.iter().enumerate() {
     434         1018 :         if s.connection as usize >= request.connections.len() {
     435            1 :             state.report(NONE, i as u32, None, "unknown planner connection".into(), true);
     436         1017 :         }
     437         1018 :         if !script_ids.insert((s.connection, s.script.catalog_entry_id())) {
     438            1 :             state.report(NONE, i as u32, None, "duplicate planner script".into(), true);
     439         1017 :         }
     440         1018 :         if !s.name.is_empty() && !names.insert(s.name) {
     441            1 :             state.report(NONE, i as u32, None, format!("duplicate script name: {}", s.name), true);
     442         1017 :         }
     443              :     }
     444          516 :     let mut files: BTreeSet<&str> = BTreeSet::new();
     445          516 :     for a in request.attachments {
     446           34 :         if !files.insert(a.file) {
     447            1 :             state.report(NONE, NONE, None, format!("duplicate attachment: {}", a.file), true);
     448           33 :         }
     449              :     }
     450          516 :     let targets = seeds(request);
     451          968 :     if targets.iter().chain(request.materialized).any(|&t| t as usize >= script_count) {
     452            1 :         state.report(NONE, NONE, None, "unknown planner script".into(), true);
     453          515 :     }
     454              :     // An invalid request stops every plan, also a tolerant one
     455          516 :     if !state.graph.valid || state.partial {
     456            6 :         state.graph.valid = false;
     457            6 :         return finish(state, request, &[], &[], ScriptReferences::new(script_count));
     458          510 :     }
     459              : 
     460              :     // Prepare the scripts: all of them in a plan of the session, also those whose results the host provides, the
     461              :     // targets and the scripts they reference in a plan of targets
     462          510 :     let mut prepared: Vec<Option<Prepared>> = (0..script_count).map(|_| None).collect();
     463          510 :     let mut planned = vec![false; script_count];
     464          510 :     let mut references = ScriptReferences::new(script_count);
     465          510 :     let mut pending: Vec<u32> = if targeted { targets.clone() } else { (0..script_count as u32).collect() };
     466         1765 :     while let Some(i) = pending.pop() {
     467         1255 :         if planned[i as usize] {
     468          260 :             continue;
     469          995 :         }
     470          995 :         planned[i as usize] = true;
     471          995 :         let input = &request.scripts[i as usize];
     472          995 :         let catalog = request.connections[input.connection as usize].catalog;
     473          995 :         let Some(p) = prepare(&mut state, i, input.script, catalog) else { continue };
     474          981 :         if !targeted {
     475          767 :             diagnose_syntax(&mut state, i, &p);
     476          883 :         }
     477          981 :         references.resolve(&mut state_reporter(&mut state), request, i, &p);
     478          981 :         pending.extend(references.of(i).iter().filter_map(|r| r.producer));
     479          981 :         prepared[i as usize] = Some(p);
     480              :     }
     481              :     // Invalid input snapshots must never yield executable partial plans
     482          510 :     if !state.graph.valid {
     483           51 :         return finish(state, request, &prepared, &planned, references);
     484          459 :     }
     485          459 :     references.check(&mut state_reporter(&mut state), request, &prepared);
     486          459 :     if tolerant {
     487           48 :         references.invalidate_unprepared(&prepared);
     488          452 :     }
     489          459 :     if !state.graph.valid {
     490           30 :         return finish(state, request, &prepared, &planned, references);
     491          429 :     }
     492              : 
     493              :     // The statements of the plan
     494          429 :     let mut include = vec![Include::None; script_count];
     495              :     {
     496          779 :         let mut pending: Vec<(u32, Include)> = targets.iter().map(|&t| (t, Include::All)).collect();
     497         1408 :         while let Some((i, level)) = pending.pop() {
     498          979 :             if include[i as usize] >= level {
     499           72 :                 continue;
     500          907 :             }
     501          907 :             include[i as usize] = level;
     502              :             // The references of the included statements and of the terminal query, which an inline reference inlines
     503          907 :             for r in references.of(i) {
     504          242 :                 let Some(producer) = r.producer else { continue };
     505           46 :                 match r.kind {
     506          163 :                     ReferenceKind::Inline => pending.push((producer, Include::Inlined)),
     507           46 :                     ReferenceKind::Scan if request.materialized.contains(&producer) => {}
     508           37 :                     ReferenceKind::Scan => pending.push((producer, Include::All)),
     509            7 :                     ReferenceKind::Invalid | ReferenceKind::Attachment => {}
     510              :                 }
     511              :             }
     512              :         }
     513              :     }
     514              :     // A plan includes every statement of a script or none
     515         1978 :     let included = |i: usize| include[i] == Include::All;
     516              :     // Scripts that a plan of targets passes through as one statement, they don't parse
     517          832 :     let opaque: Vec<bool> = (0..script_count).map(|i| targeted && include[i] == Include::All && prepared[i].as_ref().is_some_and(|p| !p.parses())).collect();
     518              : 
     519              :     // The node ids of the included statements
     520          429 :     let mut node_of: BTreeMap<(u32, u32), u32> = BTreeMap::new();
     521          429 :     let mut node_count = 0u32;
     522          832 :     for i in 0..script_count {
     523          832 :         let Some(p) = prepared[i].as_ref() else { continue };
     524          812 :         if opaque[i] {
     525            4 :             node_of.insert((i as u32, 0), node_count);
     526            4 :             node_count += 1;
     527            4 :             continue;
     528          808 :         }
     529          808 :         if !included(i) {
     530           35 :             continue;
     531          773 :         }
     532         1135 :         for statement_id in 0..p.parsed.statements.len() as u32 {
     533         1135 :             node_of.insert((i as u32, statement_id), node_count);
     534         1135 :             node_count += 1;
     535         1135 :         }
     536              :     }
     537          832 :     let participates = |i: usize| node_of.range((i as u32, 0)..=(i as u32, u32::MAX)).next().is_some();
     538              : 
     539              :     // The relations of every connection
     540          486 :     let mut connections: Vec<ConnectionState> = (0..request.connections.len()).map(|_| ConnectionState::default()).collect();
     541              :     // Catalog entry ids are unique within the catalog of a connection
     542          429 :     let participating_ids: BTreeSet<(u32, CatalogEntryId)> = (0..script_count)
     543          832 :         .filter(|&i| participates(i))
     544          774 :         .map(|i| (request.scripts[i].connection, request.scripts[i].script.catalog_entry_id()))
     545          429 :         .collect();
     546          486 :     for (ci, connection) in request.connections.iter().enumerate() {
     547          486 :         let c = &mut connections[ci];
     548          487 :         for (_, entry) in connection.catalog.entries_ranked() {
     549          405 :             c.views.extend(tables(entry).iter().filter(|t| t.is_view).map(|t| t.table_id().pack()));
     550          479 :             for table in tables(entry) {
     551          478 :                 c.declarations.insert(table.object_id, table);
     552          478 :                 c.published_views.insert(table.object_id, table.is_view);
     553          478 :             }
     554              :         }
     555          980 :         for (i, s) in request.scripts.iter().enumerate() {
     556          980 :             let Some(p) = prepared[i].as_ref().filter(|_| s.connection as usize == ci) else { continue };
     557          812 :             let analyzed = p.analyzed;
     558          812 :             c.views.extend(analyzed.entry.table_declarations.iter().filter(|t| t.is_view).map(|t| t.table_id().pack()));
     559          812 :             for table in &analyzed.entry.table_declarations {
     560          148 :                 if c.published_views.get(&table.object_id).is_some_and(|&v| v != table.is_view) {
     561            0 :                     c.incompatible_kinds.insert(table.object_id);
     562          148 :                 }
     563          148 :                 c.declarations.insert(table.object_id, table);
     564          148 :                 let node = table.ast_statement_id.and_then(|statement| node_of.get(&(i as u32, statement)));
     565          148 :                 if let Some(&node) = node {
     566          148 :                     c.producers.insert(table.table_id().pack(), node);
     567          148 :                 }
     568              :             }
     569              :         }
     570          487 :         for (_, entry) in connection.catalog.entries_ranked() {
     571              :             // Participating declarations enter the namespace only when their CREATE is reached
     572          405 :             if participating_ids.contains(&(ci as u32, entry.data().catalog_entry_id)) {
     573          136 :                 continue;
     574          269 :             }
     575          332 :             for table in tables(entry) {
     576          332 :                 c.live_namespace.entry(resource_name(&table.table_name)).or_insert(table);
     577          332 :             }
     578              :         }
     579              :     }
     580              : 
     581              :     // The materializations: the scripts that a reference of the LOCAL connection scans
     582          429 :     let mut materializations: Vec<ExecutionGraphMaterializationT> = Vec::new();
     583          429 :     let mut materialization_of: BTreeMap<u32, u32> = BTreeMap::new();
     584          832 :     for i in 0..script_count as u32 {
     585          832 :         for r in references.of(i) {
     586          228 :             if include[i as usize] == Include::None {
     587            3 :                 continue;
     588          225 :             }
     589          225 :             let (ReferenceKind::Scan, Some(producer)) = (r.kind, r.producer) else { continue };
     590           43 :             materialization_of.entry(producer).or_insert_with(|| {
     591           40 :                 let mut m = ExecutionGraphMaterializationT::default();
     592           40 :                 m.script = producer;
     593           40 :                 let terminal = prepared[producer as usize].as_ref().and_then(Prepared::terminal);
     594           40 :                 m.node = terminal.and_then(|t| node_of.get(&(producer, t)).copied()).unwrap_or(NONE);
     595           40 :                 m.path = Some(match request.scripts[producer as usize].materialization_file {
     596            1 :                     Some(file) => format!("{}/{}", request.mount_prefix, file),
     597           39 :                     None => format!("{}/s{}.parquet", request.mount_prefix, producer),
     598              :                 });
     599           40 :                 materializations.push(m);
     600           40 :                 (materializations.len() - 1) as u32
     601           40 :             });
     602              :         }
     603              :     }
     604              : 
     605          429 :     let mut nodes: Vec<ExecutionGraphNodeT> = Vec::new();
     606          429 :     let mut graph_references: Vec<ExecutionGraphScriptReferenceT> = Vec::new();
     607              :     // Edges of script references: (from, to, reason)
     608          429 :     let mut reference_edges: Vec<(u32, u32, Reason, u32)> = Vec::new();
     609          832 :     for si in 0..script_count {
     610          832 :         let Some(p) = prepared[si].as_ref() else { continue };
     611          812 :         let connection = request.scripts[si].connection;
     612          812 :         let (scanned, parsed, analyzed) = (p.scanned, p.parsed, p.analyzed);
     613          812 :         if opaque[si] {
     614              :             // A script that does not parse runs as written, as one statement
     615            4 :             let node_id = nodes.len() as u32;
     616            4 :             let mut node = ExecutionGraphNodeT::default();
     617            4 :             node.script = si as u32;
     618            4 :             node.statement_id = 0;
     619            4 :             node.statement_type = parsed.statements.first().map(|s| s.statement_type).unwrap_or(Type::NONE);
     620            4 :             node.sql = opt(Some(read_executable_text(scanned, TextSpan::new(0, scanned.text.len() as u32)).trim().to_string()));
     621            4 :             node.produces_output = true;
     622            4 :             node.script_result = true;
     623            4 :             node.barrier = true;
     624            4 :             node.connection = connection;
     625            4 :             state.diagnose(node_id, si as u32, "the script does not parse, it runs as written".into());
     626            4 :             state.spans.push(parsed.associate_descriptions().first().map(|d| d.statement_span).unwrap_or_default());
     627            4 :             nodes.push(node);
     628            4 :             continue;
     629          808 :         }
     630          808 :         let text_at = |offset: u32, length: u32| -> String {
     631            0 :             let text = scanned.text.as_bytes();
     632            0 :             let begin = (offset as usize).min(text.len());
     633            0 :             let end = (begin + length as usize).min(text.len());
     634            0 :             String::from_utf8_lossy(&text[begin..end]).into_owned()
     635            0 :         };
     636          808 :         let descriptions = parsed.associate_descriptions();
     637          808 :         let terminal = p.terminal();
     638         1170 :         for (statement_id, statement) in parsed.statements.iter().enumerate() {
     639         1170 :             if !included(si) {
     640           35 :                 continue;
     641         1135 :             }
     642         1135 :             let node_id = nodes.len() as u32;
     643         1135 :             debug_assert_eq!(node_of.get(&(si as u32, statement_id as u32)), Some(&node_id));
     644         1135 :             let c = &mut connections[connection as usize];
     645         1135 :             let mut node = ExecutionGraphNodeT::default();
     646         1135 :             node.script = si as u32;
     647         1135 :             node.statement_id = statement_id as u32;
     648         1135 :             node.statement_type = statement.statement_type;
     649         1135 :             node.connection = connection;
     650         1135 :             let ty = statement.statement_type;
     651         1135 :             let span = descriptions[statement_id].statement_span;
     652         1135 :             state.spans.push(span);
     653         1135 :             node.produces_output = produces_output(parsed, statement);
     654         1135 :             node.script_result = terminal == Some(statement_id as u32) && node.produces_output;
     655         1135 :             if node.script_result {
     656          618 :                 if let Some(&m) = materialization_of.get(&(si as u32)) {
     657           31 :                     node.materialization = m;
     658          587 :                 }
     659          517 :             }
     660              :             // The SQL of the statement as written, with the script references spliced in
     661         1135 :             let mut spliced = script_references::splice_statement(
     662         1135 :                 request,
     663         1135 :                 &prepared,
     664         1135 :                 &references,
     665         1135 :                 si as u32,
     666         1135 :                 statement_id as u32,
     667         1135 :                 &materialization_of,
     668         1135 :                 &materializations,
     669         1135 :                 &state.inputs,
     670              :             );
     671              :             // The inputs of the statement become literals, an input without a value fails the plan
     672         1135 :             let dialect = request.connections[connection as usize].dialect;
     673         1135 :             let bindings = bind_inputs(&state.inputs, p, Some(statement_id as u32), dialect);
     674         1135 :             spliced.edits.extend(bindings.edits);
     675         1135 :             for (span, message) in bindings.errors {
     676           15 :                 state.report(node_id, si as u32, Some(span), message, true);
     677           15 :             }
     678         1135 :             for (script, span, message) in std::mem::take(&mut spliced.input_errors) {
     679            0 :                 state.report(node_id, script, Some(span), message, true);
     680            0 :             }
     681         1135 :             let mut sql = if spliced.edits.is_empty() { read_executable_text(scanned, span) } else { read_executable_text_with(scanned, span, &spliced.edits) };
     682         1135 :             for r in &spliced.references {
     683          215 :                 let mut reference = ExecutionGraphScriptReferenceT::default();
     684          215 :                 reference.node = node_id;
     685          215 :                 reference.consumer_script = si as u32;
     686          215 :                 reference.text_span = Some(text_span_t(r.reference.text_span));
     687          215 :                 reference.script = r.reference.producer.unwrap_or(NONE);
     688          215 :                 reference.attachment = r.reference.attachment.unwrap_or(NONE);
     689          215 :                 reference.parameter = Some(r.reference.name.clone());
     690          215 :                 reference.cte_name = Some(r.cte_name.clone());
     691          215 :                 reference.materialization = r.materialization.unwrap_or(NONE);
     692          215 :                 graph_references.push(reference);
     693          215 :             }
     694         1135 :             let root = &parsed.nodes[statement.root as usize];
     695         1135 :             let mut explain_executes = false;
     696         1135 :             let mut accesses: BTreeMap<(u32, u32), Mode> = BTreeMap::new();
     697         1135 :             let mut barrier = false;
     698              :             // A statement that the planner can't classify is a barrier, it runs in session order
     699              :             macro_rules! barrier {
     700              :                 ($message:expr) => {{
     701              :                     barrier = true;
     702              :                     state.diagnose(node_id, si as u32, $message.to_string());
     703              :                 }};
     704              :             }
     705         1135 :             let access = |state: &mut PlanState, accesses: &mut BTreeMap<(u32, u32), Mode>, name: &Name, table: Option<&TableDeclaration>, mode: Mode| {
     706          965 :                 let target = match table {
     707          924 :                     Some(t) => (state.intern_entity(connection, t), NONE),
     708           41 :                     None => (NONE, state.intern_unresolved(connection, name)),
     709              :                 };
     710          965 :                 let entry = accesses.entry(target).or_insert(mode);
     711          965 :                 if mode > *entry {
     712            7 :                     *entry = mode;
     713          958 :                 }
     714          965 :                 target
     715          965 :             };
     716           51 :             match ty {
     717              :                 Type::SELECT
     718              :                 | Type::INSERT
     719              :                 | Type::DELETE
     720              :                 | Type::EXPLAIN
     721              :                 | Type::CREATE_TABLE
     722              :                 | Type::CREATE_TABLE_AS
     723              :                 | Type::CREATE_VIEW
     724              :                 | Type::SELECT_INTO
     725              :                 | Type::DROP_TABLE
     726         1077 :                 | Type::DROP_VIEW => {}
     727              :                 // A client function call doesn't run on the backend, see below
     728           81 :                 Type::CALL if analyzed.client_function_calls.iter().any(|c| c.ast_statement_id == statement_id as u32) => {}
     729           14 :                 _ => {
     730           14 :                     barrier!("unsupported statement effects; backend session semantics are not guaranteed");
     731           14 :                 }
     732              :             }
     733              :             // A spec shadows its namespace, a call in it never reaches the backend: the host runs its description, `sql`
     734              :             // only tells what the statement says. Any other call goes to the backend as written.
     735         1135 :             let client_function_call = analyzed.client_function_calls.iter().any(|c| c.ast_statement_id == statement_id as u32);
     736         1135 :             if client_function_call {
     737           51 :                 barrier = true;
     738           51 :                 node.client_function_call = describe_call(&state.inputs, analyzed, statement_id as u32);
     739         1084 :             }
     740              :             // A query with `@vis` tags carries its visualization
     741         1135 :             let visualization = analyzed.visualization_specs.iter().find(|s| s.ast_statement_id == Some(statement_id as u32));
     742         1135 :             if let Some(spec) = visualization.filter(|_| ty == Type::SELECT) {
     743           31 :                 match compile_visualization(spec, analyzed) {
     744           31 :                     Ok(mut compiled) => {
     745           31 :                         compiled.renderer = opt(compiled.renderer.take());
     746           31 :                         compiled.vegalite_spec = opt(compiled.vegalite_spec.take());
     747           31 :                         node.visualization = Some(Box::new(compiled));
     748           31 :                     }
     749            0 :                     Err(e) => {
     750            0 :                         barrier = true;
     751            0 :                         state.report(node_id, si as u32, None, format!("visualization: {e}"), true);
     752            0 :                         sql.clear();
     753            0 :                     }
     754              :                 }
     755         1104 :             }
     756         1135 :             if ty == Type::EXPLAIN {
     757           77 :                 let [body, options] = attrs!(parsed.children(root), [Key::EXT_EXPLAIN_STATEMENT, Key::EXT_EXPLAIN_OPTIONS]);
     758           77 :                 explain_executes = crate::touched_relations::explain_executes(parsed, options);
     759           77 :                 if body.is_none_or(|b| b.node_type() == NodeType::NONE) {
     760            0 :                     barrier!("EXPLAIN requires a statement or expressions body");
     761           77 :                 }
     762         1058 :             }
     763         1135 :             for e in &analyzed.errors {
     764           26 :                 let Some(error_node) = e.ast_node_id else { continue };
     765           21 :                 if error_node < statement.nodes_begin || error_node >= statement.nodes_begin + statement.node_count {
     766           12 :                     continue;
     767            9 :                 }
     768              :                 // No backend gets to judge a client function call, so an error of its checks stops the plan
     769            9 :                 let stops = client_function_call && e.severity == AnalyzerErrorSeverity::ERROR;
     770            9 :                 state.report(node_id, si as u32, None, format!("analyzer: {}", e.message), stops);
     771            9 :                 if e.severity == AnalyzerErrorSeverity::ERROR {
     772            9 :                     barrier = true;
     773            9 :                 }
     774              :             }
     775              :             // AST effects that are not represented by ordinary relation reads, reported once per statement
     776         1135 :             let mut effects = false;
     777        13787 :             for ni in statement.nodes_begin..statement.nodes_begin + statement.node_count {
     778        13787 :                 let ast = &parsed.nodes[ni as usize];
     779        13787 :                 if ast.node_type() == NodeType::NONE {
     780            0 :                     continue;
     781        13787 :                 }
     782        13787 :                 let mutating = matches!(
     783        13787 :                     ast.node_type(),
     784              :                     NodeType::OBJECT_SQL_INSERT
     785              :                         | NodeType::OBJECT_SQL_DELETE
     786              :                         | NodeType::OBJECT_SQL_CREATE
     787              :                         | NodeType::OBJECT_SQL_CREATE_AS
     788              :                         | NodeType::OBJECT_SQL_VIEW
     789              :                         | NodeType::OBJECT_SQL_CREATE_FUNCTION
     790        13503 :                 ) || ast.attribute_key() == Key::SQL_SELECT_INTO;
     791        13787 :                 if ty == Type::EXPLAIN && explain_executes && mutating {
     792           12 :                     barrier!("execution-enabled EXPLAIN with a mutating body cannot be safely classified");
     793        13775 :                 }
     794        13787 :                 if ast.node_type() == NodeType::ENUM_SQL_TEMP_TYPE && ast.children_begin_or_value() == TempType::NONE.0 as u32 {
     795            8 :                     continue;
     796        13779 :                 }
     797        13775 :                 if matches!(
     798        13779 :                     ast.attribute_key(),
     799              :                     Key::SQL_CREATE_TABLE_TEMP
     800              :                         | Key::SQL_CREATE_AS_TEMP
     801              :                         | Key::SQL_VIEW_TEMP
     802              :                         | Key::SQL_TEMP_TYPE
     803              :                         | Key::SQL_SELECT_ROW_LOCKING
     804              :                         | Key::SQL_TABLE_CONSTRAINT_REFERENCES_NAME
     805        13775 :                 ) || matches!(ast.node_type(), NodeType::OBJECT_SQL_GRAPH_TABLE | NodeType::OBJECT_SQL_PROPERTY_GRAPH)
     806              :                 {
     807           10 :                     if !effects {
     808            7 :                         barrier!("unsupported session, locking, constraint, or graph effects");
     809            7 :                     }
     810           10 :                     effects = true;
     811        13769 :                 }
     812              :             }
     813         1726 :             for table_ref in &analyzed.table_references {
     814              :                 // A table parameter reads its script through the script references
     815         1661 :                 if table_ref.ast_statement_id != Some(statement_id as u32) || table_ref.reads_table_parameter() {
     816          973 :                     continue;
     817          688 :                 }
     818          688 :                 let Some(relation) = &table_ref.inner else {
     819            0 :                     barrier!("unsupported table source");
     820            0 :                     continue;
     821              :                 };
     822          688 :                 if !relation.resolved_alternatives.is_empty() {
     823            7 :                     barrier!("ambiguous relation reference");
     824          681 :                 }
     825              :                 // References to CTEs are no relation accesses
     826          688 :                 if is_cte_reference(analyzed, table_ref) {
     827           39 :                     continue;
     828          649 :                 }
     829          649 :                 let (name, table) = c.resolve(relation);
     830          649 :                 if relation.resolved_table.is_some() && table.is_none() {
     831            0 :                     barrier!("resolved relation declaration is incompatible or unavailable");
     832          649 :                 }
     833          649 :                 let write = table_ref.role == TableRole::Write && (ty != Type::EXPLAIN || explain_executes);
     834          649 :                 access(&mut state, &mut accesses, &name, table, if write { Mode::WRITE } else { Mode::READ });
     835          649 :                 if c.dropped_resources.contains(&name) {
     836            7 :                     barrier!("relation was dropped without an earlier lifetime producer; later producer cannot be reordered");
     837          642 :                 }
     838          649 :                 match &relation.resolved_table {
     839           35 :                     None => barrier!(format!("unresolved relation: {}", relation.table_name.debug_string())),
     840          614 :                     Some(resolved) => {
     841          614 :                         let id = match table {
     842          614 :                             Some(t) => t.table_id().pack(),
     843            0 :                             None => resolved.catalog_table_id.unpack_table_id().pack(),
     844              :                         };
     845          614 :                         if c.producers.get(&id).is_some_and(|&p| p >= node_id) && !c.live_producers.contains(&name) {
     846            7 :                             barrier!("relation resolves to a later producer in input order");
     847          607 :                         }
     848          614 :                         if c.views.contains(&id) || c.live_views.contains(&name) {
     849           24 :                             barrier!("view dependencies are not expanded; conservative access barrier");
     850          590 :                         }
     851              :                     }
     852              :                 }
     853              :             }
     854              :             // An inline script reference reads the relations of the terminal query of the referenced script
     855         1135 :             let direct: BTreeSet<(u32, u32)> = accesses.keys().copied().collect();
     856         1135 :             let mut inherited: BTreeSet<(u32, u32)> = BTreeSet::new();
     857         1135 :             for &(producer, producer_statement) in &spliced.inlined {
     858          153 :                 let producer_analyzed = prepared[producer as usize].as_ref().unwrap().analyzed;
     859          153 :                 for table_ref in &producer_analyzed.table_references {
     860          124 :                     if table_ref.ast_statement_id != Some(producer_statement)
     861          124 :                         || is_cte_reference(producer_analyzed, table_ref)
     862          121 :                         || table_ref.reads_table_parameter()
     863              :                     {
     864            3 :                         continue;
     865          121 :                     }
     866          121 :                     let Some(relation) = &table_ref.inner else { continue };
     867          121 :                     let (name, table) = c.resolve(relation);
     868          121 :                     let target = access(&mut state, &mut accesses, &name, table, Mode::READ);
     869          121 :                     if !direct.contains(&target) {
     870          113 :                         inherited.insert(target);
     871          113 :                     }
     872          121 :                     if relation.resolved_table.is_none() {
     873            0 :                         barrier!(format!("unresolved relation: {}", relation.table_name.debug_string()));
     874          121 :                     }
     875              :                 }
     876              :             }
     877         1135 :             for &m in &spliced.inputs {
     878           46 :                 let producer_node = materializations[m as usize].node;
     879           46 :                 if producer_node != NONE {
     880           37 :                     reference_edges.push((producer_node, node_id, Reason::MATERIALIZATION, materializations[m as usize].script));
     881           37 :                 }
     882              :             }
     883         1135 :             node.inputs = (!spliced.inputs.is_empty()).then(|| spliced.inputs.clone());
     884         1135 :             node.inlined_scripts = (!spliced.inlined.is_empty()).then(|| spliced.inlined.iter().map(|&(producer, _)| producer).collect());
     885         1135 :             node.attachments = (!spliced.attachments.is_empty()).then(|| spliced.attachments.clone());
     886         1135 :             let drop = matches!(ty, Type::DROP_TABLE | Type::DROP_VIEW);
     887         1135 :             let ddl = drop || matches!(ty, Type::CREATE_TABLE | Type::CREATE_TABLE_AS | Type::CREATE_VIEW | Type::SELECT_INTO);
     888         1135 :             if ddl {
     889          195 :                 let packed = pack::pack_parsed_statement(parsed, statement_id);
     890          195 :                 match packed.target {
     891            0 :                     None => barrier!("missing DDL target"),
     892          195 :                     Some(target) => {
     893          195 :                         let mut name: Name = (
     894          195 :                             target.database_name.clone().unwrap_or_default(),
     895          195 :                             target.schema_name.clone().unwrap_or_default(),
     896          195 :                             target.relation_name.clone().unwrap_or_default(),
     897          195 :                         );
     898          195 :                         let mut table: Option<&TableDeclaration> = None;
     899          195 :                         if drop {
     900              :                             // Prefer an exact live name, then native suffix matches. Never let a future
     901              :                             // declaration in an analyzed input redirect this DROP.
     902           47 :                             if !c.live_namespace.contains_key(&name) {
     903           15 :                                 let matches: BTreeSet<Name> = c
     904           15 :                                     .live_namespace
     905           15 :                                     .keys()
     906           15 :                                     .filter(|n| n.2 == name.2 && (name.1.is_empty() || n.1 == name.1) && (name.0.is_empty() || n.0 == name.0))
     907           15 :                                     .cloned()
     908           15 :                                     .collect();
     909           15 :                                 if matches.len() == 1 {
     910            9 :                                     name = matches.into_iter().next().unwrap();
     911            9 :                                 } else if matches.len() > 1 {
     912            3 :                                     barrier!("ambiguous DROP target");
     913            3 :                                 }
     914           32 :                             }
     915           47 :                             match c.live_namespace.get(&name) {
     916           41 :                                 Some(&live) => table = Some(live),
     917            6 :                                 None => barrier!("unresolved DROP target; conservative access barrier"),
     918              :                             }
     919           47 :                             if let Some(t) = table {
     920           41 :                                 if c.views.contains(&t.table_id().pack()) || c.live_views.contains(&name) {
     921            9 :                                     barrier!("view dependencies are not expanded; conservative access barrier");
     922           32 :                                 }
     923            6 :                             }
     924              :                         } else {
     925          176 :                             for declaration in &analyzed.entry.table_declarations {
     926          176 :                                 if declaration.ast_statement_id == Some(statement_id as u32) && resource_name(&declaration.table_name) == name {
     927          148 :                                     table = Some(declaration);
     928          148 :                                 }
     929              :                             }
     930          148 :                             match table {
     931          148 :                                 Some(t) => name = resource_name(&t.table_name),
     932            0 :                                 None => barrier!("missing DDL target declaration"),
     933              :                             }
     934              :                         }
     935          195 :                         let target = access(&mut state, &mut accesses, &name, table, Mode::DDL);
     936          195 :                         inherited.remove(&target);
     937          195 :                         if drop {
     938           47 :                             c.live_producers.remove(&name);
     939           47 :                             c.live_namespace.remove(&name);
     940           47 :                             c.live_views.remove(&name);
     941           47 :                             c.dropped_resources.insert(name);
     942           47 :                         } else {
     943          148 :                             c.live_producers.insert(name.clone());
     944          148 :                             if let Some(t) = table {
     945          148 :                                 c.live_namespace.insert(name.clone(), t);
     946          148 :                             }
     947          148 :                             c.dropped_resources.remove(&name);
     948          148 :                             if ty == Type::CREATE_VIEW {
     949           17 :                                 c.live_views.insert(name);
     950          131 :                             } else {
     951          131 :                                 c.live_views.remove(&name);
     952          131 :                             }
     953              :                         }
     954              :                     }
     955              :                 }
     956          940 :             }
     957         1135 :             state.inherited.extend(inherited.into_iter().map(|(entity, unresolved)| (node_id, entity, unresolved)));
     958         1135 :             node.accesses = opt_vec(
     959         1135 :                 accesses
     960         1135 :                     .into_iter()
     961         1135 :                     .map(|((entity, unresolved), mode)| {
     962          944 :                         let mut a = ExecutionGraphAccessT::default();
     963          944 :                         a.entity = entity;
     964          944 :                         a.unresolved_reference = unresolved;
     965          944 :                         a.mode = mode;
     966          944 :                         a
     967          944 :                     })
     968         1135 :                     .collect(),
     969              :             );
     970         1135 :             node.sql = opt(Some(sql));
     971         1135 :             node.barrier = barrier;
     972         1135 :             nodes.push(node);
     973              :         }
     974              :     }
     975              : 
     976              :     // Order conflicting accesses along the timeline of every relation
     977              :     /// The accesses of a relation that later accesses must be ordered after
     978              :     #[derive(Default)]
     979              :     struct Timeline {
     980              :         /// The node that wrote the relation last
     981              :         writer: Option<u32>,
     982              :         /// The access mode of the last writer
     983              :         mode: Option<Mode>,
     984              :         /// The nodes that read the relation since the last write
     985              :         readers: Vec<u32>,
     986              :     }
     987          429 :     let mut timelines: BTreeMap<(u32, Name), Timeline> = BTreeMap::new();
     988          429 :     let mut edges: BTreeSet<(u32, u32, u32, u32, u8)> = BTreeSet::new();
     989         2520 :     let mut edge = |from: Option<u32>, to: u32, entity: u32, unresolved: u32, reason: Reason| {
     990         2520 :         if let Some(from) = from {
     991          637 :             if from != to {
     992          637 :                 edges.insert((from, to, entity, unresolved, reason.0));
     993          637 :             }
     994         1883 :         }
     995         2520 :     };
     996              :     // Barriers order the statements of their connection
     997          429 :     let mut last_barrier: BTreeMap<u32, u32> = BTreeMap::new();
     998          429 :     let mut since_barrier: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
     999          429 :     let mut previous_of_script: BTreeMap<u32, u32> = BTreeMap::new();
    1000         1139 :     for (i, node) in nodes.iter().enumerate() {
    1001         1139 :         let i = i as u32;
    1002         1139 :         let connection = node.connection;
    1003              :         // A plan of targets runs the statements of a script in order
    1004         1139 :         if targeted {
    1005          235 :             edge(previous_of_script.insert(node.script, i), i, NONE, NONE, Reason::SEQUENCE);
    1006         1043 :         }
    1007         1139 :         edge(last_barrier.get(&connection).copied(), i, NONE, NONE, Reason::BARRIER);
    1008         1139 :         let since = since_barrier.entry(connection).or_default();
    1009         1139 :         if node.barrier {
    1010          182 :             for &earlier in since.iter() {
    1011           88 :                 edge(Some(earlier), i, NONE, NONE, Reason::BARRIER);
    1012           88 :             }
    1013          180 :             since.clear();
    1014          180 :             last_barrier.insert(connection, i);
    1015          959 :         } else {
    1016          959 :             since.push(i);
    1017          959 :         }
    1018              :         // Merge by name only for conflict tracking
    1019         1139 :         let mut merged: BTreeMap<Name, &ExecutionGraphAccessT> = BTreeMap::new();
    1020         1139 :         for a in node.accesses.iter().flatten() {
    1021          944 :             let name = if a.entity != NONE {
    1022          903 :                 let e = &state.entity_list[a.entity as usize];
    1023          903 :                 (e.database_name.clone().unwrap_or_default(), e.schema_name.clone().unwrap_or_default(), e.name.clone().unwrap_or_default())
    1024              :             } else {
    1025           41 :                 let r = &state.unresolved_list[a.unresolved_reference as usize];
    1026           41 :                 (r.database_name.clone().unwrap_or_default(), r.schema_name.clone().unwrap_or_default(), r.name.clone().unwrap_or_default())
    1027              :             };
    1028              :             // A statement can write `t` and the catalog can know it as `db.schema.t`
    1029          944 :             let defaults = &request.connections[connection as usize];
    1030          944 :             let name = (
    1031          944 :                 if name.0.is_empty() { defaults.default_database.to_string() } else { name.0 },
    1032          944 :                 if name.1.is_empty() { defaults.default_schema.to_string() } else { name.1 },
    1033          944 :                 name.2,
    1034              :             );
    1035          944 :             match merged.get(&name) {
    1036            0 :                 Some(existing) if a.mode <= existing.mode => {}
    1037          944 :                 _ => {
    1038          944 :                     merged.insert(name, a);
    1039          944 :                 }
    1040              :             }
    1041              :         }
    1042         1139 :         for (name, a) in merged {
    1043          944 :             let timeline = timelines.entry((connection, name)).or_default();
    1044          944 :             let ddl = a.mode == Mode::DDL || timeline.mode == Some(Mode::DDL);
    1045          944 :             if a.mode == Mode::READ {
    1046          635 :                 edge(timeline.writer, i, a.entity, a.unresolved_reference, if ddl { Reason::DDL } else { Reason::READ_AFTER_WRITE });
    1047          635 :                 timeline.readers.push(i);
    1048              :             } else {
    1049          309 :                 edge(timeline.writer, i, a.entity, a.unresolved_reference, if ddl { Reason::DDL } else { Reason::WRITE_AFTER_WRITE });
    1050          309 :                 for &reader in &timeline.readers {
    1051           80 :                     edge(Some(reader), i, a.entity, a.unresolved_reference, if a.mode == Mode::DDL { Reason::DDL } else { Reason::WRITE_AFTER_READ });
    1052              :                 }
    1053          309 :                 timeline.readers.clear();
    1054          309 :                 timeline.writer = Some(i);
    1055          309 :                 timeline.mode = Some(a.mode);
    1056              :             }
    1057              :         }
    1058              :     }
    1059              :     // A scan reference needs the result of the referenced script before it in the session
    1060          429 :     for (from, to, reason, producer) in reference_edges {
    1061           37 :         if from < to {
    1062           34 :             edge(Some(from), to, NONE, NONE, reason);
    1063           34 :         } else {
    1064            3 :             let consumer = nodes[to as usize].script;
    1065            3 :             let message = format!(
    1066            3 :                 "{} references {}, which runs later; move {} above {}",
    1067            3 :                 script_label(request, consumer),
    1068            3 :                 script_label(request, producer),
    1069            3 :                 script_label(request, producer),
    1070            3 :                 script_label(request, consumer)
    1071            3 :             );
    1072            3 :             state.report(to, consumer, None, message, true);
    1073            3 :         }
    1074              :     }
    1075          429 :     let edges = edges
    1076          429 :         .into_iter()
    1077          643 :         .map(|(from, to, entity, unresolved, reason)| {
    1078          637 :             let mut e = ExecutionGraphEdgeT::default();
    1079          637 :             e.from = from;
    1080          637 :             e.to = to;
    1081          637 :             e.entity = entity;
    1082          637 :             e.unresolved_reference = unresolved;
    1083          637 :             e.reason = Reason(reason);
    1084          637 :             e
    1085          637 :         })
    1086          429 :         .collect();
    1087          429 :     state.graph.nodes = opt_vec(nodes);
    1088          429 :     state.graph.edges = opt_vec(edges);
    1089          429 :     state.graph.materializations = opt_vec(materializations);
    1090          429 :     state.graph.references = opt_vec(graph_references);
    1091          429 :     finish(state, request, &prepared, &planned, references)
    1092          516 : }
    1093              : 
    1094              : /// The label of a script in diagnostics, `foo.sql`
    1095         1045 : pub(crate) fn script_label(request: &SessionPlanRequest, script: u32) -> String {
    1096         1045 :     match request.scripts.get(script as usize).map(|s| s.name).filter(|n| !n.is_empty()) {
    1097         1045 :         Some(name) => format!("{name}.sql"),
    1098            0 :         None => format!("script {script}"),
    1099              :     }
    1100         1045 : }
    1101              : 
    1102              : /// Does a table reference read a common table expression?
    1103          812 : fn is_cte_reference(analyzed: &AnalyzedScript, table_ref: &dashql_core::analyzer::TableReference) -> bool {
    1104          812 :     let Some(relation) = &table_ref.inner else { return false };
    1105          812 :     if relation.resolved_table.is_some()
    1106           77 :         || table_ref.role != TableRole::Read
    1107           77 :         || !relation.table_name.database_name.text.is_empty()
    1108           65 :         || !relation.table_name.schema_name.text.is_empty()
    1109              :     {
    1110          750 :         return false;
    1111           62 :     }
    1112           62 :     let Some(&scope) = table_ref.ast_scope_root.and_then(|r| analyzed.name_scopes_by_root_node.get(&r)) else { return false };
    1113           62 :     let alias = match &table_ref.alias {
    1114            6 :         Some((a, _)) => a.text.clone(),
    1115           56 :         None => relation.table_name.table_name.text.clone(),
    1116              :     };
    1117           62 :     matches!(analyzed.name_scopes[scope as usize].source(&alias).map(|s| &s.table), Some(ReferencedTable::Cte(_)))
    1118          812 : }
    1119              : 
    1120          215 : fn text_span_t(span: TextSpan) -> TextSpanT {
    1121          215 :     let mut t = TextSpanT::default();
    1122          215 :     t.offset = span.offset;
    1123          215 :     t.length = span.length;
    1124          215 :     t
    1125          215 : }
    1126              : 
    1127              : /// A reporter for the diagnostics of script references
    1128              : struct Reporter<'s> {
    1129              :     state: &'s mut PlanState,
    1130              : }
    1131              : 
    1132              : impl ReferenceErrors for Reporter<'_> {
    1133           95 :     fn error(&mut self, script: u32, text_span: Option<TextSpan>, message: String) {
    1134           95 :         self.state.report(NONE, script, text_span, message, true);
    1135           95 :     }
    1136              : }
    1137              : 
    1138         1440 : fn state_reporter(state: &mut PlanState) -> Reporter<'_> {
    1139         1440 :     Reporter { state }
    1140         1440 : }
    1141              : 
    1142              : /// A string that is left out of the packed buffer when empty
    1143         5656 : fn opt(s: Option<String>) -> Option<String> {
    1144         5656 :     s.filter(|s| !s.is_empty())
    1145         5656 : }
    1146              : 
    1147              : /// A vector that is left out of the packed buffer when empty
    1148         6928 : fn opt_vec<T>(v: Vec<T>) -> Option<Vec<T>> {
    1149         6928 :     (!v.is_empty()).then_some(v)
    1150         6928 : }
    1151              : 
    1152              : /// Complete the graph of a plan with its connections, scripts, entities and diagnostics
    1153          516 : fn finish(state: PlanState, request: &SessionPlanRequest, prepared: &[Option<Prepared>], planned: &[bool], references: ScriptReferences) -> Plan {
    1154          516 :     let targets: BTreeSet<u32> = seeds(request).into_iter().collect();
    1155          516 :     let mut graph = state.graph;
    1156          516 :     if state.partial {
    1157           11 :         graph.valid = false;
    1158          505 :     }
    1159          516 :     graph.connections = opt_vec(
    1160          516 :         request
    1161          516 :             .connections
    1162          516 :             .iter()
    1163          592 :             .map(|c| {
    1164          592 :                 let mut out = ExecutionGraphConnectionT::default();
    1165          592 :                 out.connection_id = opt(Some(c.id.to_string()));
    1166          592 :                 out.kind = match c.kind {
    1167          478 :                     ConnectionKind::Remote => ExecutionGraphConnectionKind::REMOTE,
    1168          114 :                     ConnectionKind::Local => ExecutionGraphConnectionKind::LOCAL,
    1169              :                 };
    1170          592 :                 out
    1171          592 :             })
    1172          516 :             .collect(),
    1173              :     );
    1174          516 :     graph.attachments = opt_vec(
    1175          516 :         request
    1176          516 :             .attachments
    1177          516 :             .iter()
    1178          516 :             .enumerate()
    1179          516 :             .map(|(i, a)| {
    1180           34 :                 let mut out = ExecutionGraphAttachmentT::default();
    1181           34 :                 out.name = opt(Some(a.name.to_string()));
    1182           34 :                 out.file = opt(Some(a.file.to_string()));
    1183           34 :                 out.path = opt(Some(script_references::attachment_path(request, i as u32)));
    1184           34 :                 out
    1185           34 :             })
    1186          516 :             .collect(),
    1187              :     );
    1188          516 :     graph.scripts = opt_vec(
    1189          516 :         request
    1190          516 :             .scripts
    1191          516 :             .iter()
    1192          516 :             .enumerate()
    1193         1018 :             .map(|(i, s)| {
    1194         1018 :                 let mut meta = ExecutionGraphScriptT::default();
    1195         1018 :                 let script = s.script;
    1196         1018 :                 meta.script_id = script.catalog_entry_id();
    1197         1018 :                 meta.text_version = script.text_version();
    1198         1018 :                 if let Some(analyzed) = script.analyzed.as_ref() {
    1199         1018 :                     meta.analyzed_text_version = analyzed.parsed.scanned.text_version;
    1200         1018 :                     meta.analyzed_catalog_revision = analyzed.snapshot.version() as u64;
    1201         1018 :                 }
    1202         1018 :                 meta.name = opt(Some(s.name.to_string()));
    1203         1018 :                 meta.connection = s.connection;
    1204         1018 :                 meta.planned = planned.get(i).copied().unwrap_or(false);
    1205         1018 :                 meta.target = targets.contains(&(i as u32));
    1206         1018 :                 if let Some(p) = prepared.get(i).and_then(Option::as_ref) {
    1207              :                     // The signature of the AST, the values that the script binds, `:min=10` per line, and the stamps of the
    1208              :                     // attachments it reads, `attachment sales.parquet=<stamp>` per line. The keys of the scripts it
    1209              :                     // references cover what they read.
    1210          981 :                     meta.signature = opt(dashql_core::signature::compute_signature(p.parsed).ok().map(|signature| {
    1211              :                         // The values in the Hyper dialect, the key covers the connection
    1212          965 :                         let values = bind_inputs(&state.inputs, p, None, FormattingDialect::HYPER).values;
    1213          965 :                         let attachments: BTreeSet<(&str, &str)> = references
    1214          965 :                             .of(i as u32)
    1215          965 :                             .iter()
    1216          965 :                             .filter_map(|r| request.attachments.get(r.attachment? as usize))
    1217          965 :                             .map(|a| (a.file, a.stamp))
    1218          965 :                             .collect();
    1219          965 :                         std::iter::once(signature)
    1220          965 :                             .chain(values.iter().map(|(name, value)| format!(":{name}={value}")))
    1221          965 :                             .chain(attachments.iter().map(|(file, stamp)| format!("attachment {file}={stamp}")))
    1222          965 :                             .collect::<Vec<_>>()
    1223          965 :                             .join("\n")
    1224          965 :                     }));
    1225              :                     // Queries only, a client function call asks a service and not the backend. The scripts that a script
    1226              :                     // references are single queries or calls, or the plan is invalid, and the key of a script that
    1227              :                     // references a call isn't reusable either.
    1228          981 :                     meta.cacheable = p.parses()
    1229          965 :                         && !p.parsed.statements.is_empty()
    1230         1139 :                         && p.parsed.statements.iter().all(|s| matches!(s.statement_type, Type::SELECT | Type::EXPLAIN))
    1231          666 :                         && p.analyzed.client_function_calls.is_empty();
    1232          981 :                     meta.terminal_statement_id = p.terminal().unwrap_or(NONE);
    1233          981 :                     meta.referenced_scripts = opt_vec(references.producers_of(i as u32));
    1234           37 :                 } else {
    1235           37 :                     meta.terminal_statement_id = NONE;
    1236           37 :                 }
    1237         1018 :                 meta
    1238         1018 :             })
    1239          516 :             .collect(),
    1240              :     );
    1241          516 :     graph.entities = opt_vec(state.entity_list);
    1242          516 :     graph.unresolved_references = opt_vec(state.unresolved_list);
    1243          516 :     graph.diagnostics = opt_vec(state.diagnostics);
    1244          516 :     Plan { graph, references, inputs: state.inputs, spans: state.spans, inherited: state.inherited }
    1245          516 : }
    1246              : 
    1247              : #[cfg(test)]
    1248              : mod tests {
    1249              :     use super::*;
    1250              : 
    1251           12 :     fn analyzed(catalog: &Catalog, text: &str) -> Script {
    1252           12 :         let mut script = Script::new(catalog);
    1253           12 :         script.replace_text(text);
    1254           12 :         script.scan();
    1255           12 :         script.parse();
    1256           12 :         script.analyze(catalog, false).unwrap();
    1257           12 :         script
    1258           12 :     }
    1259              : 
    1260            9 :     fn messages(graph: &ExecutionGraphT) -> Vec<String> {
    1261            9 :         graph.diagnostics.iter().flatten().filter_map(|d| d.message.clone()).collect()
    1262            9 :     }
    1263              : 
    1264           20 :     fn request<'a>(connections: &'a [PlannerConnection<'a>], scripts: &'a [PlannerScript<'a>]) -> SessionPlanRequest<'a> {
    1265           20 :         SessionPlanRequest { connections, scripts, scope: PlanScope::Session, materialized: &[], mount_prefix: "/mnt/external/test", attachments: &[], attachment_prefix: "" }
    1266           20 :     }
    1267              : 
    1268              :     #[test]
    1269            1 :     fn rejects_duplicate_script_names() {
    1270            1 :         let catalog = Catalog::new();
    1271            1 :         let (a, b) = (analyzed(&catalog, "select 1"), analyzed(&catalog, "select 2"));
    1272            1 :         let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Local, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1273            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }, PlannerScript { script: &b, name: "foo", connection: 0, materialization_file: None }];
    1274            1 :         let graph = plan_session(&request(&connections, &scripts));
    1275            1 :         assert!(!graph.valid);
    1276            1 :         assert!(graph.nodes.is_none());
    1277            1 :         assert_eq!(messages(&graph), vec!["duplicate script name: foo"]);
    1278            1 :     }
    1279              : 
    1280              :     #[test]
    1281            1 :     fn rejects_connections_that_share_a_catalog() {
    1282            1 :         let catalog = Catalog::new();
    1283            1 :         let a = analyzed(&catalog, "select 1");
    1284            1 :         let connections = [
    1285            1 :             PlannerConnection { id: "first", kind: ConnectionKind::Local, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" },
    1286            1 :             PlannerConnection { id: "second", kind: ConnectionKind::Remote, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" },
    1287            1 :         ];
    1288            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }];
    1289            1 :         let graph = plan_session(&request(&connections, &scripts));
    1290            1 :         assert!(!graph.valid);
    1291            1 :         assert_eq!(messages(&graph), vec!["connections share a catalog: second"]);
    1292            1 :     }
    1293              : 
    1294              :     #[test]
    1295            1 :     fn rejects_unknown_connections_and_scripts() {
    1296            1 :         let catalog = Catalog::new();
    1297            1 :         let a = analyzed(&catalog, "select 1");
    1298            1 :         let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Local, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1299            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 1, materialization_file: None }];
    1300            1 :         assert_eq!(messages(&plan_session(&request(&connections, &scripts))), vec!["unknown planner connection"]);
    1301            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }];
    1302            1 :         let mut targets = request(&connections, &scripts);
    1303            1 :         targets.scope = PlanScope::Targets(&[3]);
    1304            1 :         assert_eq!(messages(&plan_session(&targets)), vec!["unknown planner script"]);
    1305            1 :     }
    1306              : 
    1307              :     #[test]
    1308            1 :     fn names_materializations_after_the_files_of_the_host() {
    1309            1 :         let (trino, local) = (Catalog::new(), Catalog::new());
    1310            1 :         let (foo, bar) = (analyzed(&trino, "select 1 as a"), analyzed(&local, "select * from :foo"));
    1311            1 :         let connections = [
    1312            1 :             PlannerConnection { id: "trino", kind: ConnectionKind::Remote, catalog: &trino, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" },
    1313            1 :             PlannerConnection { id: "local", kind: ConnectionKind::Local, catalog: &local, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" },
    1314            1 :         ];
    1315            1 :         let scripts = [
    1316            1 :             PlannerScript { script: &foo, name: "foo", connection: 0, materialization_file: Some("abc.0123.parquet") },
    1317            1 :             PlannerScript { script: &bar, name: "bar", connection: 1, materialization_file: None },
    1318            1 :         ];
    1319            1 :         let graph = plan_session(&request(&connections, &scripts));
    1320            1 :         assert!(graph.valid, "{:?}", messages(&graph));
    1321            1 :         let path = graph.materializations.as_ref().unwrap()[0].path.clone().unwrap();
    1322            1 :         assert_eq!(path, "/mnt/external/test/abc.0123.parquet");
    1323            1 :         assert!(graph.nodes.as_ref().unwrap()[1].sql.as_ref().unwrap().contains("external('/mnt/external/test/abc.0123.parquet'"));
    1324            1 :     }
    1325              : 
    1326              :     #[test]
    1327            1 :     fn rejects_duplicate_scripts_and_attachments() {
    1328            1 :         let catalog = Catalog::new();
    1329            1 :         let a = analyzed(&catalog, "select 1");
    1330            1 :         let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Local, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1331              :         // The same script under two names
    1332            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }, PlannerScript { script: &a, name: "bar", connection: 0, materialization_file: None }];
    1333            1 :         let graph = plan_session(&request(&connections, &scripts));
    1334            1 :         assert!(!graph.valid);
    1335            1 :         assert_eq!(messages(&graph), vec!["duplicate planner script"]);
    1336              :         // Two attachments of one file
    1337            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }];
    1338            1 :         let attachments = [PlannerAttachment { name: "x", file: "x.parquet", stamp: "1" }, PlannerAttachment { name: "y", file: "x.parquet", stamp: "1" }];
    1339            1 :         let graph = plan_session(&SessionPlanRequest { attachments: &attachments, ..request(&connections, &scripts) });
    1340            1 :         assert!(!graph.valid);
    1341            1 :         assert_eq!(messages(&graph), vec!["duplicate attachment: x.parquet"]);
    1342            1 :     }
    1343              : 
    1344              :     #[test]
    1345            1 :     fn rejects_analyses_of_an_older_catalog_revision() {
    1346            1 :         let mut catalog = Catalog::new();
    1347            1 :         let a = analyzed(&catalog, "select * from t");
    1348              :         // Publishing another script makes the analysis stale
    1349            1 :         let schema = analyzed(&catalog, "create table t (x int)");
    1350            1 :         catalog.load_script(schema.analyzed.clone().unwrap(), dashql_core::catalog::CatalogLayer::SESSION).unwrap();
    1351            1 :         let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Local, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1352            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }];
    1353            1 :         let graph = plan_session(&request(&connections, &scripts));
    1354            1 :         assert!(!graph.valid);
    1355            1 :         assert_eq!(messages(&graph), vec!["stale catalog analysis"]);
    1356              :         // A tolerant plan takes the older analysis
    1357            1 :         let tolerant = plan_session(&SessionPlanRequest { scope: PlanScope::Tolerant, ..request(&connections, &scripts) });
    1358            1 :         assert!(!messages(&tolerant).contains(&"stale catalog analysis".to_string()));
    1359            1 :     }
    1360              : 
    1361              :     #[test]
    1362            1 :     fn rejects_scripts_of_another_catalog() {
    1363            1 :         let (catalog, other) = (Catalog::new(), Catalog::new());
    1364            1 :         let a = analyzed(&other, "select 1");
    1365            1 :         let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Local, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1366            1 :         let scripts = [PlannerScript { script: &a, name: "foo", connection: 0, materialization_file: None }];
    1367            1 :         let graph = plan_session(&request(&connections, &scripts));
    1368            1 :         assert!(!graph.valid);
    1369            1 :         assert_eq!(messages(&graph), vec!["script was analyzed with another catalog than the one of its connection"]);
    1370            1 :     }
    1371              : 
    1372              :     #[test]
    1373            1 :     fn caches_the_results_of_scripts_of_queries() {
    1374              :         // A cache hit skips the whole script, so every statement must be a query
    1375            9 :         for (text, cacheable) in [
    1376            1 :             ("select 1", true),
    1377            1 :             ("explain select 1", true),
    1378            1 :             ("select 1; select 2", true),
    1379            1 :             ("create table t as select 1 as a; select a from t", false),
    1380            1 :             ("set search_path = x; select 1", false),
    1381            1 :             ("create table t (a int)", false),
    1382            1 :             ("select 1 +;", false),
    1383            1 :             ("call x.y ()", false),
    1384            1 :             ("explain call clfn.f ()", false),
    1385            1 :         ] {
    1386            9 :             let mut catalog = Catalog::new();
    1387            9 :             catalog.register_client_functions(r#"{ "format": 1, "namespace": "clfn", "functions": [{ "name": "f" }] }"#).unwrap();
    1388            9 :             let mut script = Script::new(&catalog);
    1389            9 :             script.replace_text(text);
    1390            9 :             script.scan();
    1391            9 :             script.parse();
    1392            9 :             script.analyze(&catalog, false).unwrap();
    1393            9 :             let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Remote, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1394            9 :             let scripts = [PlannerScript { script: &script, name: "foo", connection: 0, materialization_file: None }];
    1395            9 :             let mut targets = request(&connections, &scripts);
    1396            9 :             targets.scope = PlanScope::Targets(&[0]);
    1397            9 :             let graph = plan_session(&targets);
    1398            9 :             assert!(graph.valid, "{text}: {:?}", messages(&graph));
    1399            9 :             assert_eq!(graph.scripts.as_ref().unwrap()[0].cacheable, cacheable, "{text}");
    1400              :         }
    1401            1 :     }
    1402              : 
    1403              :     #[test]
    1404            1 :     fn plans_scripts_without_names() {
    1405            1 :         let catalog = Catalog::new();
    1406            1 :         let (a, b) = (analyzed(&catalog, "select 1"), analyzed(&catalog, "select 2"));
    1407            1 :         let connections = [PlannerConnection { id: "c", kind: ConnectionKind::Remote, catalog: &catalog, dialect: FormattingDialect::HYPER, default_database: "", default_schema: "" }];
    1408            1 :         let scripts = [PlannerScript { script: &a, name: "", connection: 0, materialization_file: None }, PlannerScript { script: &b, name: "", connection: 0, materialization_file: None }];
    1409            1 :         let graph = plan_session(&request(&connections, &scripts));
    1410            1 :         assert!(graph.valid);
    1411            1 :         assert_eq!(graph.nodes.as_ref().map(Vec::len), Some(2));
    1412            1 :         assert!(graph.connections.as_ref().is_some_and(|c| c.len() == 1 && c[0].kind == ExecutionGraphConnectionKind::REMOTE));
    1413            1 :     }
    1414              : }
        

Generated by: LCOV version 2.0-1