Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/sqlquack-semantics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ sqlquack-syntax.workspace = true
sqlquack-core.workspace = true
sqlquack-snowflake.workspace = true

[dev-dependencies]
sqlquack-syntax-postgres.workspace = true

[lints]
workspace = true
12 changes: 10 additions & 2 deletions crates/sqlquack-semantics/src/lineage/_helpers/findings/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ pub(crate) fn column_finding(
),
));
}
if matches >= 1 || current.sources.iter().any(unjudged) {
if matches >= 1
|| current
.sources
.iter()
.any(|source| unjudged(source) || source.query.faulted)
{
return None;
}
scope = current.outer;
Expand Down Expand Up @@ -120,7 +125,10 @@ fn qualified_finding(
let mut scope: Option<&LineageScope<'_>> = Some(level.scope);
while let Some(current) = scope {
if let Some(source) = source_named(current, &alias, context) {
return (!unjudged(source) && !offers(source, wanted, context)).then(|| {
return (!unjudged(source)
&& !source.query.faulted
&& !offers(source, wanted, context))
.then(|| {
(
UNKNOWN_COLUMN_CODE,
format!(
Expand Down
61 changes: 52 additions & 9 deletions crates/sqlquack-semantics/src/lineage/_helpers/findings/levels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,61 @@ pub(crate) fn query_findings(
}
}

/// The CTEs `with` adds to `ctes`, and each body's findings made with the CTEs before it.
/// The CTEs `with` adds to `ctes`, each body's findings, and under PostgreSQL each one faulted.
fn visited_ctes<'a>(
with: Option<&With>,
(ctes, outer, aliases): (&'a [NamedQuery], Option<&LineageScope<'_>>, &[String]),
context: LineageContext<'_>,
) -> (Cow<'a, [NamedQuery]>, Vec<NameFinding>) {
let mut found: Vec<NameFinding> = Vec::new();
let postgres: bool = Engine::of(context) == Engine::Postgres;
let reach = with_ctes_visiting(with, (ctes, outer), context, &mut |cte, reach| {
found.extend(query_findings(&cte.this, (reach, outer, aliases), context));
let body: Vec<NameFinding> = query_findings(&cte.this, (reach, outer, aliases), context);
let faulted: bool =
postgres && (!body.is_empty() || reads_faulted(&cte.this, reach, context));
found.extend(body);
faulted
});
(reach, found)
}

/// The key of a derived table in FROM, by its alias; none when `item` is not a derived table.
fn derived_key(item: &Expression, context: LineageContext<'_>) -> Option<Option<String>> {
match item {
Expression::Subquery(subquery) => {
Some(subquery.alias.as_ref().map(|alias| key(alias, context)))
}
Expression::Alias(alias) if matches!(alias.this, Expression::Subquery(_)) => {
Some(Some(key(&alias.alias, context)))
}
_ => None,
}
}

/// Whether `query` names a faulted CTE among `reach`.
fn reads_faulted(query: &Expression, reach: &[NamedQuery], context: LineageContext<'_>) -> bool {
query.dfs().any(|node| match node {
Expression::Table(table) if table.schema.is_none() && table.catalog.is_none() => {
faulted_cte(&key(&table.name, context), reach, context)
}
_ => false,
})
}

/// Whether the CTE `name` resolves to among `reach` is faulted.
fn faulted_cte(name: &str, reach: &[NamedQuery], context: LineageContext<'_>) -> bool {
reach
.iter()
.any(|named| named.query.faulted && context.profile.same_identifier(&named.key, name))
}

fn select_findings(
(node, select): (&Expression, &Select),
(ctes, outer, aliases): (&[NamedQuery], Option<&LineageScope<'_>>, &[String]),
context: LineageContext<'_>,
) -> Vec<NameFinding> {
let (ctes, mut nested) = visited_ctes(select.with.as_ref(), (ctes, outer, aliases), context);
let scope: LineageScope<'_> = level_scope(select, &ctes, outer, context);
let mut scope: LineageScope<'_> = level_scope(select, &ctes, outer, context);
let level = NodeLevel {
scope: &scope,
context,
Expand All @@ -97,13 +132,22 @@ fn select_findings(
}
let mut own: Vec<NameFinding> = alias_count_findings(select, &scope, context);
own.extend(using_findings(select, &scope, context));
let postgres: bool = Engine::of(context) == Engine::Postgres;
for item in from_items(select) {
let reads: Option<&LineageScope<'_>> = lateral(item).then_some(&scope);
nested.extend(query_findings(
unannotated(item),
(&ctes, reads, aliases),
context,
));
let body: Vec<NameFinding> =
query_findings(unannotated(item), (&ctes, reads, aliases), context);
if let Some(derived) = derived_key(unannotated(item), context)
&& postgres
&& (!body.is_empty() || reads_faulted(item, &ctes, context))
{
scope
.sources
.iter_mut()
.filter(|source| source.query.derived && source.key == derived)
.for_each(|source| source.query.faulted = true);
}
nested.extend(body);
own.extend(unpivot_findings(item, &ctes, context));
}
let mut outputs: Vec<String> = aliases.to_vec();
Expand All @@ -124,7 +168,6 @@ fn select_findings(
)
}),
};
let postgres: bool = Engine::of(context) == Engine::Postgres;
let unnamed: [String; 0] = [];
for projection in &select.expressions {
let own_name: String = output_key(unannotated(projection), context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,31 +274,32 @@ pub(crate) fn with_ctes<'a>(
outer: Option<&LineageScope<'_>>,
context: LineageContext<'_>,
) -> Cow<'a, [NamedQuery]> {
with_ctes_visiting(with, (ctes, outer), context, &mut |_, _| {})
with_ctes_visiting(with, (ctes, outer), context, &mut |_, _| false)
}

/// `with_ctes`, showing `visit` each CTE with the CTEs its body can read.
pub(crate) fn with_ctes_visiting<'a>(
with: Option<&With>,
(ctes, outer): (&'a [NamedQuery], Option<&LineageScope<'_>>),
context: LineageContext<'_>,
visit: &mut dyn FnMut(&Cte, &[NamedQuery]),
visit: &mut dyn FnMut(&Cte, &[NamedQuery]) -> bool,
) -> Cow<'a, [NamedQuery]> {
let Some(with) = with else {
return Cow::Borrowed(ctes);
};
let mut reach: Vec<NamedQuery> = ctes.to_vec();
for cte in &with.ctes {
visit(cte, &reach);
let faulted: bool = visit(cte, &reach);
let name = key(&cte.alias, context);
let query: ResolvedQuery = match with.recursive || reads_itself(cte, &name, context) {
let mut query: ResolvedQuery = match with.recursive || reads_itself(cte, &name, context) {
true => recursive_query(cte, (&name, &reach), outer, context),
false => renamed(
resolved_query(&cte.this, &reach, outer, context),
&cte.columns,
context,
),
};
query.faulted = faulted;
reach.retain(|named| named.key != name);
reach.push(NamedQuery { key: name, query });
}
Expand Down Expand Up @@ -709,6 +710,7 @@ pub(crate) fn expanded_star(
rows: RowReads::default(),
outer_nullable: only.is_some_and(|only| only.query.outer_nullable),
derived: false,
faulted: false,
}
}

Expand Down
Loading
Loading