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