Skip to main content

uqa_sql/semantics/
privileges.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Semantic base-column privilege analysis for query sources.
8
9pub mod columns;
10mod enforcement;
11
12use crate::catalog::roles::RoleReference;
13use std::collections::{BTreeMap, BTreeSet};
14
15use crate::plan::{QueryBlockPlan, QueryPlan, RelationalPlan, SourcePlan};
16use crate::SQLError;
17use crate::ScalarExpr;
18
19pub mod context;
20use context::PrivilegeScope as CteScope;
21use enforcement::ensure_required_select;
22
23#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
24struct BaseColumn {
25    table: String,
26    column: String,
27}
28
29#[derive(Clone, Debug)]
30struct OutputColumn {
31    name: String,
32    sources: BTreeSet<BaseColumn>,
33}
34
35#[derive(Clone, Default)]
36struct SourceLineage {
37    output: Vec<OutputColumn>,
38    qualifiers: BTreeMap<String, Vec<OutputColumn>>,
39    system_qualifiers: BTreeMap<String, BTreeSet<String>>,
40    tables: BTreeMap<String, BTreeSet<String>>,
41}
42
43impl SourceLineage {
44    fn has_unqualified(&self, column: &str) -> bool {
45        self.output.iter().any(|output| output.name == column)
46    }
47
48    fn has_qualifier(&self, qualifier: &str) -> bool {
49        self.qualifiers.contains_key(qualifier) || self.system_qualifiers.contains_key(qualifier)
50    }
51
52    fn require_unqualified(&self, column: &str, required: &mut BTreeSet<BaseColumn>) {
53        for output in self.output.iter().filter(|output| output.name == column) {
54            required.extend(output.sources.iter().cloned());
55        }
56    }
57
58    fn require_qualified(
59        &self,
60        qualifier: &str,
61        column: &str,
62        required: &mut BTreeSet<BaseColumn>,
63    ) {
64        if let Some(columns) = self.qualifiers.get(qualifier) {
65            for output in columns.iter().filter(|output| output.name == column) {
66                required.extend(output.sources.iter().cloned());
67            }
68        }
69    }
70
71    fn require_all(&self, required: &mut BTreeSet<BaseColumn>) {
72        for output in &self.output {
73            required.extend(output.sources.iter().cloned());
74        }
75    }
76
77    fn require_qualified_all(&self, qualifier: &str, required: &mut BTreeSet<BaseColumn>) {
78        if let Some(columns) = self.qualifiers.get(qualifier) {
79            for output in columns {
80                required.extend(output.sources.iter().cloned());
81            }
82        }
83    }
84
85    fn require_unqualified_system(&self, column: &str, required: &mut BTreeSet<BaseColumn>) {
86        for table in self.system_qualifiers.values().flatten() {
87            required.insert(BaseColumn {
88                table: table.clone(),
89                column: column.to_string(),
90            });
91        }
92    }
93
94    fn require_qualified_system(
95        &self,
96        qualifier: &str,
97        column: &str,
98        required: &mut BTreeSet<BaseColumn>,
99    ) {
100        if let Some(tables) = self.system_qualifiers.get(qualifier) {
101            required.extend(tables.iter().cloned().map(|table| BaseColumn {
102                table,
103                column: column.to_string(),
104            }));
105        }
106    }
107
108    fn include_tables(&mut self, other: &Self) {
109        for (table, columns) in &other.tables {
110            self.tables
111                .entry(table.clone())
112                .or_default()
113                .extend(columns.iter().cloned());
114        }
115    }
116}
117
118fn opaque_lineage(columns: Vec<String>, qualifier: Option<String>) -> SourceLineage {
119    let output = columns
120        .into_iter()
121        .map(|name| OutputColumn {
122            name,
123            sources: BTreeSet::new(),
124        })
125        .collect::<Vec<_>>();
126    let qualifiers = qualifier.map_or_else(BTreeMap::new, |qualifier| {
127        BTreeMap::from([(qualifier, output.clone())])
128    });
129    SourceLineage {
130        output,
131        qualifiers,
132        system_qualifiers: BTreeMap::new(),
133        tables: BTreeMap::new(),
134    }
135}
136
137fn is_system_column(column: &str) -> bool {
138    matches!(
139        column,
140        "ctid" | "xmin" | "cmin" | "xmax" | "cmax" | "tableoid"
141    )
142}
143
144fn rename_output_columns(output: &mut [OutputColumn], aliases: &[String]) {
145    for (column, alias) in output.iter_mut().zip(aliases) {
146        column.name.clone_from(alias);
147    }
148}
149
150fn table_lineage(
151    name: &str,
152    qualifier: &str,
153    alias: Option<&str>,
154    column_aliases: &[String],
155    ctes: &CteScope<'_>,
156) -> Result<SourceLineage, SQLError> {
157    if ctes.is_visible_cte(name) {
158        let columns = ctes
159            .materialized_for_scan(name)
160            .or_else(|| {
161                ctes.deferred_reference(name)
162                    .and_then(|cte| cte.body.query())
163                    .and_then(crate::semantics::projection::query_plan_output_columns)
164            })
165            .unwrap_or_default();
166        let mut output = opaque_lineage(columns, Some(alias.unwrap_or(qualifier).to_string()));
167        rename_output_columns(&mut output.output, column_aliases);
168        for columns in output.qualifiers.values_mut() {
169            rename_output_columns(columns, column_aliases);
170        }
171        return Ok(output);
172    }
173    let catalog = ctes.catalog;
174    let resolution = ctes.resolution.clone();
175    let Some(relation) = catalog.relation(&resolution, name)? else {
176        return Ok(SourceLineage::default());
177    };
178    let canonical = relation.canonical;
179    let columns = relation.columns;
180    let system_columns = relation.kind.has_system_columns();
181    let visible_qualifier = alias.unwrap_or(qualifier).to_string();
182    let mut output = columns
183        .iter()
184        .map(|column| OutputColumn {
185            name: column.clone(),
186            sources: BTreeSet::from([BaseColumn {
187                table: canonical.clone(),
188                column: column.clone(),
189            }]),
190        })
191        .collect::<Vec<_>>();
192    rename_output_columns(&mut output, column_aliases);
193    Ok(SourceLineage {
194        output: output.clone(),
195        qualifiers: BTreeMap::from([(visible_qualifier, output)]),
196        system_qualifiers: if system_columns {
197            BTreeMap::from([(
198                alias.unwrap_or(qualifier).to_string(),
199                BTreeSet::from([canonical.clone()]),
200            )])
201        } else {
202            BTreeMap::new()
203        },
204        tables: BTreeMap::from([(canonical, columns.into_iter().collect())]),
205    })
206}
207
208fn join_lineage(
209    left: SourceLineage,
210    right: SourceLineage,
211    using: Option<&crate::ast::JoinUsing>,
212    natural: bool,
213    alias: Option<&str>,
214    column_aliases: &[String],
215) -> SourceLineage {
216    let using_columns = using.map_or_else(
217        || {
218            if natural {
219                left.output
220                    .iter()
221                    .filter(|left| right.output.iter().any(|right| right.name == left.name))
222                    .map(|column| column.name.clone())
223                    .collect::<Vec<_>>()
224            } else {
225                Vec::new()
226            }
227        },
228        |using| using.columns.clone(),
229    );
230    let using_set = using_columns.iter().collect::<BTreeSet<_>>();
231    let mut output = Vec::new();
232    for column in &using_columns {
233        let mut sources = BTreeSet::new();
234        for candidate in left
235            .output
236            .iter()
237            .chain(&right.output)
238            .filter(|candidate| candidate.name == *column)
239        {
240            sources.extend(candidate.sources.iter().cloned());
241        }
242        output.push(OutputColumn {
243            name: column.clone(),
244            sources,
245        });
246    }
247    output.extend(
248        left.output
249            .iter()
250            .chain(&right.output)
251            .filter(|column| !using_set.contains(&column.name))
252            .cloned(),
253    );
254    let mut tables = left.tables.clone();
255    for (table, columns) in &right.tables {
256        tables
257            .entry(table.clone())
258            .or_default()
259            .extend(columns.iter().cloned());
260    }
261    let (qualifiers, system_qualifiers) = if let Some(alias) = alias {
262        rename_output_columns(&mut output, column_aliases);
263        (
264            BTreeMap::from([(alias.to_string(), output.clone())]),
265            BTreeMap::new(),
266        )
267    } else {
268        let mut qualifiers = left.qualifiers;
269        qualifiers.extend(right.qualifiers);
270        let mut system_qualifiers = left.system_qualifiers;
271        system_qualifiers.extend(right.system_qualifiers);
272        (qualifiers, system_qualifiers)
273    };
274    SourceLineage {
275        output,
276        qualifiers,
277        system_qualifiers,
278        tables,
279    }
280}
281
282fn collect_expression_columns(
283    expression: &ScalarExpr,
284    scopes: &[SourceLineage],
285    direct_star: bool,
286    required: &mut BTreeSet<BaseColumn>,
287) -> BTreeSet<usize> {
288    if direct_star {
289        match expression {
290            ScalarExpr::Star => {
291                if let Some(scope) = scopes.iter().find(|scope| !scope.output.is_empty()) {
292                    scope.require_all(required);
293                }
294                return BTreeSet::new();
295            }
296            ScalarExpr::QualifiedStar(qualifier) => {
297                if let Some(scope) = scopes.iter().find(|scope| scope.has_qualifier(qualifier)) {
298                    scope.require_qualified_all(qualifier, required);
299                }
300                return BTreeSet::new();
301            }
302            _ => {}
303        }
304    }
305    let mut subquery_ids = BTreeSet::new();
306    expression.visit(&mut |node| match node {
307        ScalarExpr::Column(column) => {
308            if let Some(scope) = scopes.iter().find(|scope| scope.has_unqualified(column)) {
309                scope.require_unqualified(column, required);
310            } else if is_system_column(column) {
311                if let Some(scope) = scopes
312                    .iter()
313                    .find(|scope| !scope.system_qualifiers.is_empty())
314                {
315                    scope.require_unqualified_system(column, required);
316                }
317            }
318        }
319        ScalarExpr::QualifiedColumn {
320            qualifier, column, ..
321        } => {
322            if let Some(scope) = scopes.iter().find(|scope| scope.has_qualifier(qualifier)) {
323                scope.require_qualified(qualifier, column, required);
324                if is_system_column(column) {
325                    scope.require_qualified_system(qualifier, column, required);
326                }
327            }
328        }
329        ScalarExpr::QualifiedStar(qualifier) => {
330            if let Some(scope) = scopes.iter().find(|scope| scope.has_qualifier(qualifier)) {
331                scope.require_qualified_all(qualifier, required);
332            }
333        }
334        ScalarExpr::ScalarSubquery(subquery)
335        | ScalarExpr::Exists { subquery, .. }
336        | ScalarExpr::InSubquery { subquery, .. } => {
337            subquery_ids.insert(*subquery);
338        }
339        _ => {}
340    });
341    subquery_ids
342}
343
344fn collect_expression_and_subqueries(
345    expression: &ScalarExpr,
346    scopes: &[SourceLineage],
347    direct_star: bool,
348    subqueries: &[QueryPlan],
349    ctes: &CteScope<'_>,
350    universe: &mut SourceLineage,
351    required: &mut BTreeSet<BaseColumn>,
352) -> Result<(), SQLError> {
353    for subquery in collect_expression_columns(expression, scopes, direct_star, required) {
354        let plan = subqueries.get(subquery).ok_or_else(|| {
355            SQLError::Internal(format!(
356                "column privilege analysis cannot resolve scalar subquery slot {subquery}"
357            ))
358        })?;
359        analyze_query_plan(plan, ctes, scopes, universe, required)?;
360    }
361    Ok(())
362}
363
364fn values_lineage(
365    rows: &[Vec<ScalarExpr>],
366    alias: Option<&str>,
367    column_aliases: &[String],
368) -> SourceLineage {
369    let columns = if column_aliases.is_empty() {
370        (1..=rows.first().map_or(0, Vec::len))
371            .map(|index| format!("column{index}"))
372            .collect()
373    } else {
374        column_aliases.to_vec()
375    };
376    opaque_lineage(columns, alias.map(str::to_string))
377}
378
379fn function_lineage(
380    name: &str,
381    output_name: &str,
382    alias: Option<&str>,
383    column_aliases: &[String],
384) -> SourceLineage {
385    let columns = if column_aliases.is_empty() {
386        vec![if output_name.is_empty() {
387            name.rsplit('.').next().unwrap_or(name).to_string()
388        } else {
389            output_name.to_string()
390        }]
391    } else {
392        column_aliases.to_vec()
393    };
394    let qualifier = alias.map_or_else(
395        || name.rsplit('.').next().unwrap_or(name).to_string(),
396        str::to_string,
397    );
398    opaque_lineage(columns, Some(qualifier))
399}
400
401fn analyze_join_source_lineage(
402    source: &SourcePlan,
403    outer_scopes: &[SourceLineage],
404    subqueries: &[QueryPlan],
405    ctes: &CteScope<'_>,
406    universe: &mut SourceLineage,
407    required: &mut BTreeSet<BaseColumn>,
408) -> Result<SourceLineage, SQLError> {
409    let SourcePlan::Join {
410        left,
411        right,
412        on,
413        using,
414        natural,
415        alias,
416        column_aliases,
417        ..
418    } = source
419    else {
420        unreachable!("join lineage helper requires a join source")
421    };
422    let left = analyze_source_lineage(left, outer_scopes, subqueries, ctes, universe, required)?;
423    let mut right_scopes = vec![left.clone()];
424    right_scopes.extend_from_slice(outer_scopes);
425    let right = analyze_source_lineage(right, &right_scopes, subqueries, ctes, universe, required)?;
426    let inputs = join_lineage(left.clone(), right.clone(), None, false, None, &[]);
427    let mut condition_scopes = vec![inputs];
428    condition_scopes.extend_from_slice(outer_scopes);
429    if let Some(on) = on {
430        collect_expression_and_subqueries(
431            on,
432            &condition_scopes,
433            false,
434            subqueries,
435            ctes,
436            universe,
437            required,
438        )?;
439    }
440    if let Some(using) = using {
441        for column in &using.columns {
442            left.require_unqualified(column, required);
443            right.require_unqualified(column, required);
444        }
445    } else if *natural {
446        for column in left
447            .output
448            .iter()
449            .filter(|left| right.output.iter().any(|right| right.name == left.name))
450        {
451            left.require_unqualified(&column.name, required);
452            right.require_unqualified(&column.name, required);
453        }
454    }
455    Ok(join_lineage(
456        left,
457        right,
458        using.as_ref(),
459        *natural,
460        alias.as_deref(),
461        column_aliases,
462    ))
463}
464
465fn analyze_function_group_lineage(
466    source: &SourcePlan,
467    outer_scopes: &[SourceLineage],
468    subqueries: &[QueryPlan],
469    ctes: &CteScope<'_>,
470    universe: &mut SourceLineage,
471    required: &mut BTreeSet<BaseColumn>,
472) -> Result<SourceLineage, SQLError> {
473    let SourcePlan::FunctionGroup {
474        functions,
475        alias,
476        column_aliases,
477        ..
478    } = source
479    else {
480        unreachable!("function-group lineage helper requires a function group")
481    };
482    for expression in functions.iter().flat_map(|function| &function.args) {
483        collect_expression_and_subqueries(
484            expression,
485            outer_scopes,
486            false,
487            subqueries,
488            ctes,
489            universe,
490            required,
491        )?;
492    }
493    let columns = if column_aliases.is_empty() {
494        functions
495            .iter()
496            .flat_map(|function| {
497                if function.column_aliases.is_empty() {
498                    vec![function.output_name.clone()]
499                } else {
500                    function.column_aliases.clone()
501                }
502            })
503            .collect()
504    } else {
505        column_aliases.clone()
506    };
507    Ok(opaque_lineage(columns, alias.clone()))
508}
509
510fn analyze_source_lineage(
511    source: &SourcePlan,
512    outer_scopes: &[SourceLineage],
513    subqueries: &[QueryPlan],
514    ctes: &CteScope<'_>,
515    universe: &mut SourceLineage,
516    required: &mut BTreeSet<BaseColumn>,
517) -> Result<SourceLineage, SQLError> {
518    match source {
519        SourcePlan::Table {
520            name,
521            qualifier,
522            alias,
523            column_aliases,
524            ..
525        } => table_lineage(name, qualifier, alias.as_deref(), column_aliases, ctes),
526        source @ SourcePlan::Join { .. } => {
527            analyze_join_source_lineage(source, outer_scopes, subqueries, ctes, universe, required)
528        }
529        SourcePlan::Values {
530            rows,
531            alias,
532            column_aliases,
533            ..
534        } => {
535            for expression in rows.iter().flatten() {
536                collect_expression_and_subqueries(
537                    expression,
538                    outer_scopes,
539                    false,
540                    subqueries,
541                    ctes,
542                    universe,
543                    required,
544                )?;
545            }
546            Ok(values_lineage(rows, alias.as_deref(), column_aliases))
547        }
548        SourcePlan::Function {
549            name,
550            output_name,
551            args,
552            alias,
553            column_aliases,
554            ..
555        } => {
556            for expression in args {
557                collect_expression_and_subqueries(
558                    expression,
559                    outer_scopes,
560                    false,
561                    subqueries,
562                    ctes,
563                    universe,
564                    required,
565                )?;
566            }
567            Ok(function_lineage(
568                name,
569                output_name,
570                alias.as_deref(),
571                column_aliases,
572            ))
573        }
574        source @ SourcePlan::FunctionGroup { .. } => analyze_function_group_lineage(
575            source,
576            outer_scopes,
577            subqueries,
578            ctes,
579            universe,
580            required,
581        ),
582        SourcePlan::Subquery {
583            body,
584            alias,
585            column_aliases,
586        } => {
587            analyze_query_plan(body, ctes, outer_scopes, universe, required)?;
588            let mut columns =
589                crate::semantics::projection::query_plan_output_columns(body).unwrap_or_default();
590            for (column, alias) in columns.iter_mut().zip(column_aliases) {
591                column.clone_from(alias);
592            }
593            Ok(opaque_lineage(columns, alias.clone()))
594        }
595    }
596}
597
598fn analyze_query_block(
599    block: &QueryBlockPlan,
600    source: Option<&SourcePlan>,
601    ctes: &CteScope<'_>,
602    outer_scopes: &[SourceLineage],
603    universe: &mut SourceLineage,
604    required: &mut BTreeSet<BaseColumn>,
605) -> Result<(), SQLError> {
606    let lineage = source.map_or_else(
607        || Ok(SourceLineage::default()),
608        |source| {
609            analyze_source_lineage(
610                source,
611                outer_scopes,
612                &block.subqueries,
613                ctes,
614                universe,
615                required,
616            )
617        },
618    )?;
619    universe.include_tables(&lineage);
620    let mut scopes = vec![lineage];
621    scopes.extend_from_slice(outer_scopes);
622    for column in block
623        .privilege_columns
624        .iter()
625        .filter(|_| ctes.include_authorization_only_columns)
626    {
627        let expression = column.qualifier().map_or_else(
628            || ScalarExpr::Column(column.column().to_string()),
629            |qualifier| ScalarExpr::qualified_column(qualifier, column.column()),
630        );
631        collect_expression_columns(&expression, &scopes, false, required);
632    }
633    for projection in &block.projections {
634        collect_expression_and_subqueries(
635            &projection.expr,
636            &scopes,
637            true,
638            &block.subqueries,
639            ctes,
640            universe,
641            required,
642        )?;
643    }
644    for expression in block
645        .r#where
646        .iter()
647        .chain(block.group_by.iter())
648        .chain(block.grouping_sets.iter().flatten())
649        .chain(block.having.iter())
650        .chain(block.order_by.iter().map(|order| &order.expr))
651        .chain(block.distinct_on.iter())
652        .chain(block.limit.iter())
653        .chain(block.offset.iter())
654    {
655        collect_expression_and_subqueries(
656            expression,
657            &scopes,
658            false,
659            &block.subqueries,
660            ctes,
661            universe,
662            required,
663        )?;
664    }
665    Ok(())
666}
667
668fn analyze_query_plan(
669    plan: &QueryPlan,
670    ctes: &CteScope<'_>,
671    outer_scopes: &[SourceLineage],
672    universe: &mut SourceLineage,
673    required: &mut BTreeSet<BaseColumn>,
674) -> Result<(), SQLError> {
675    let reachable = crate::semantics::reachable_plan_cte_names(plan);
676    let mut preceding_ctes = ctes.clone();
677    for cte in &plan.ctes {
678        if reachable.contains(&cte.name) {
679            let mut definition_ctes = preceding_ctes.clone();
680            if cte.recursive {
681                for local in &plan.ctes {
682                    definition_ctes.insert_deferred(local.clone());
683                }
684            }
685            if let Some(query) = cte.body.query() {
686                analyze_query_plan(query, &definition_ctes, outer_scopes, universe, required)?;
687            }
688        }
689        preceding_ctes.insert_deferred(cte.clone());
690    }
691    let mut root_ctes = ctes.clone();
692    for cte in &plan.ctes {
693        root_ctes.insert_deferred(cte.clone());
694    }
695    match &plan.root {
696        RelationalPlan::QueryBlock(block) => analyze_query_block(
697            block,
698            block.from.as_ref(),
699            &root_ctes,
700            outer_scopes,
701            universe,
702            required,
703        ),
704        RelationalPlan::SetOp {
705            left,
706            right,
707            order_by,
708            limit,
709            offset,
710            subqueries,
711            ..
712        } => {
713            analyze_query_plan(left, &root_ctes, outer_scopes, universe, required)?;
714            analyze_query_plan(right, &root_ctes, outer_scopes, universe, required)?;
715            let result_scope = opaque_lineage(
716                crate::semantics::projection::query_plan_output_columns(left).unwrap_or_default(),
717                None,
718            );
719            let mut scopes = vec![result_scope];
720            scopes.extend_from_slice(outer_scopes);
721            for expression in order_by
722                .iter()
723                .map(|order| &order.expr)
724                .chain(limit.iter().map(Box::as_ref))
725                .chain(offset.iter().map(Box::as_ref))
726            {
727                collect_expression_and_subqueries(
728                    expression, &scopes, false, subqueries, &root_ctes, universe, required,
729                )?;
730            }
731            Ok(())
732        }
733        RelationalPlan::Values { rows, subqueries } => {
734            for expression in rows.iter().flatten() {
735                collect_expression_and_subqueries(
736                    expression,
737                    outer_scopes,
738                    false,
739                    subqueries,
740                    &root_ctes,
741                    universe,
742                    required,
743                )?;
744            }
745            Ok(())
746        }
747    }
748}
749
750pub fn ensure_select_privileges_for_query_block(
751    statement: &QueryBlockPlan,
752    source: &SourcePlan,
753    ctes: &CteScope<'_>,
754) -> Result<(), SQLError> {
755    let mut universe = SourceLineage::default();
756    let mut required = BTreeSet::new();
757    analyze_query_block(
758        statement,
759        Some(source),
760        ctes,
761        &[],
762        &mut universe,
763        &mut required,
764    )?;
765    ensure_required_select(&universe, &required, ctes)
766}
767
768pub fn ensure_select_privileges_for_source_expressions(
769    source: &SourcePlan,
770    expressions: &[&ScalarExpr],
771    ctes: &CteScope<'_>,
772) -> Result<(), SQLError> {
773    let (universe, required) = source_expression_dependencies(source, expressions, ctes)?;
774    ensure_required_select(&universe, &required, ctes)
775}
776
777fn source_expression_dependencies(
778    source: &SourcePlan,
779    expressions: &[&ScalarExpr],
780    ctes: &CteScope<'_>,
781) -> Result<(SourceLineage, BTreeSet<BaseColumn>), SQLError> {
782    let mut universe = SourceLineage::default();
783    let mut required = BTreeSet::new();
784    let lineage = analyze_source_lineage(
785        source,
786        &[],
787        &ctes.scalar_subqueries,
788        ctes,
789        &mut universe,
790        &mut required,
791    )?;
792    universe.include_tables(&lineage);
793    let scopes = [lineage.clone()];
794    for expression in expressions {
795        collect_expression_and_subqueries(
796            expression,
797            &scopes,
798            matches!(expression, ScalarExpr::QualifiedStar(_)),
799            &ctes.scalar_subqueries,
800            ctes,
801            &mut universe,
802            &mut required,
803        )?;
804    }
805    Ok((universe, required))
806}
807
808pub fn ensure_select_privileges_for_table_expressions(
809    table: &str,
810    qualifiers: &BTreeSet<String>,
811    expressions: &[&ScalarExpr],
812    subqueries: &[QueryPlan],
813    required_columns: &[String],
814    ctes: &CteScope<'_>,
815) -> Result<(), SQLError> {
816    let (universe, required) = table_expression_dependencies(
817        table,
818        qualifiers,
819        expressions,
820        subqueries,
821        required_columns,
822        ctes,
823    )?;
824    ensure_required_select(&universe, &required, ctes)
825}
826
827fn table_expression_dependencies(
828    table: &str,
829    qualifiers: &BTreeSet<String>,
830    expressions: &[&ScalarExpr],
831    subqueries: &[QueryPlan],
832    required_columns: &[String],
833    ctes: &CteScope<'_>,
834) -> Result<(SourceLineage, BTreeSet<BaseColumn>), SQLError> {
835    let catalog = ctes.catalog;
836    let mut resolution = ctes.resolution.clone();
837    resolution.set_lookup_mode(crate::catalog::resolution::RelationLookupMode::Bound);
838    let Some(relation) = catalog.relation(&resolution, table)? else {
839        return Err(SQLError::UnknownTable(table.to_string()));
840    };
841    let canonical = relation.canonical;
842    let columns = relation.columns;
843    let system_columns = relation.kind.has_system_columns();
844    let output = columns
845        .iter()
846        .map(|column| OutputColumn {
847            name: column.clone(),
848            sources: BTreeSet::from([BaseColumn {
849                table: canonical.clone(),
850                column: column.clone(),
851            }]),
852        })
853        .collect::<Vec<_>>();
854    let lineage = SourceLineage {
855        output: output.clone(),
856        qualifiers: qualifiers
857            .iter()
858            .map(|qualifier| (qualifier.clone(), output.clone()))
859            .collect(),
860        system_qualifiers: if system_columns {
861            qualifiers
862                .iter()
863                .map(|qualifier| (qualifier.clone(), BTreeSet::from([canonical.clone()])))
864                .collect()
865        } else {
866            BTreeMap::new()
867        },
868        tables: BTreeMap::from([(canonical.clone(), columns.into_iter().collect())]),
869    };
870    let mut universe = SourceLineage::default();
871    let mut required = required_columns
872        .iter()
873        .cloned()
874        .map(|column| BaseColumn {
875            table: canonical.clone(),
876            column,
877        })
878        .collect::<BTreeSet<_>>();
879    let scopes = [lineage.clone()];
880    for expression in expressions {
881        collect_expression_and_subqueries(
882            expression,
883            &scopes,
884            true,
885            subqueries,
886            ctes,
887            &mut universe,
888            &mut required,
889        )?;
890    }
891    if required.iter().any(|column| column.table == canonical) {
892        universe.include_tables(&lineage);
893    }
894    Ok((universe, required))
895}
896
897pub struct TargetSelectPrivilegeRequest<'a, 'expr> {
898    pub table: &'a str,
899    pub privilege_subject: Option<&'a RoleReference>,
900    pub target_qualifier: &'a str,
901    pub returning_aliases: &'a crate::ast::ReturningAliases,
902    pub expressions: &'a [&'expr ScalarExpr],
903    pub subqueries: &'a [QueryPlan],
904    pub required_columns: &'a [String],
905}