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
46 changes: 2 additions & 44 deletions crates/sqlbuild-analysis/src/compiled_project/_helpers/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

use sqlbuild_core::text::main::is_word_char::is_word_char;

use crate::compiled_project::constants::{
CURRENT_DIRECTORY, NEWLINE, OPT_OUT_KEYS, PATH_SEPARATOR,
};
use crate::compiled_project::models::{LegacySchemaFile, TextPosition};
use crate::compiled_project::constants::{NEWLINE, OPT_OUT_KEYS};
use crate::compiled_project::models::TextPosition;

/// `_text_location`: the first whole-word opt-out key at or after character `start`.
pub(crate) fn opt_out_position(text: &str, start: usize) -> TextPosition {
Expand Down Expand Up @@ -53,43 +51,3 @@ fn starts_key(characters: &[char], index: usize) -> bool {
}
false
}

/// The error for a model a legacy schema file under its directory still declares.
pub(crate) fn legacy_schema_error(
model: (&str, &str),
schema_files: &[LegacySchemaFile],
) -> Option<String> {
let (name, relative_path) = model;
let model_parts: Vec<&str> = parts(relative_path);
let mut matches: Vec<&str> = Vec::new();
for file in schema_files {
let directory: Vec<&str> = parts(&file.relative_path);
if !model_parts.starts_with(&directory[..directory.len().saturating_sub(1)]) {
continue;
}
for entry in &file.model_names {
if entry == name {
matches.push(&file.relative_path);
}
}
}
match matches.as_slice() {
[] => None,
[file] => Some(format!(
"Schema file {file} declares model '{name}', but model metadata must live in \
{relative_path} MODEL(...). Move description, columns, and audits into the model \
header."
)),
files => Some(format!(
"Model file {relative_path} matched multiple schema.yml declarations: {}",
files.join(", ")
)),
}
}

/// A relative path's parts, as `PurePosixPath.parts` gives them (`.` has none).
fn parts(path: &str) -> Vec<&str> {
path.split(PATH_SEPARATOR)
.filter(|part| !part.is_empty() && *part != CURRENT_DIRECTORY)
.collect()
}
2 changes: 0 additions & 2 deletions crates/sqlbuild-analysis/src/compiled_project/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,4 @@ pub(crate) const INVALID_CONSTANT_REFERENCE_STOP: u8 = 3;
pub(crate) const MACRO_TOKEN: char = '@';
/// The keys whose `false` value opts a model out of SQL analysis.
pub(crate) const OPT_OUT_KEYS: [&str; 2] = ["sql_analysis", "sql_validation"];
pub(crate) const PATH_SEPARATOR: char = '/';
pub(crate) const CURRENT_DIRECTORY: &str = ".";
pub(crate) const NEWLINE: char = '\n';

This file was deleted.

1 change: 0 additions & 1 deletion crates/sqlbuild-analysis/src/compiled_project/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pub mod calls_macros;
pub mod declaration_error_message;
pub mod enum_member_literal;
pub mod header_metadata_keys;
pub mod legacy_schema_error;
pub mod located_scan_error;
pub mod merge_call_site_references;
pub mod model_facts;
Expand Down
7 changes: 0 additions & 7 deletions crates/sqlbuild-analysis/src/compiled_project/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,6 @@ pub struct TextPosition {
pub column: usize,
}

/// A legacy schema file that declares model entries, by its relative path.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LegacySchemaFile {
pub relative_path: String,
pub model_names: Vec<String>,
}

/// The authored model a reference-scan fault is located in.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ScanFaultOrigin<'a> {
Expand Down
14 changes: 0 additions & 14 deletions crates/sqlbuild-analysis/src/compiled_project/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,6 @@ pub(super) fn analysing_settings(
}
}

pub(super) fn legacy_schema_files(
files: &[(&str, &[&str])],
) -> Vec<crate::compiled_project::models::LegacySchemaFile> {
files
.iter()
.map(
|(path, names)| crate::compiled_project::models::LegacySchemaFile {
relative_path: (*path).to_owned(),
model_names: names.iter().map(|name| (*name).to_owned()).collect(),
},
)
.collect()
}

pub(super) fn generic_syntax() -> sqlbuild_sqltext::sql_scan::models::LexicalSyntax {
sqlbuild_sqltext::sql_scan::models::LexicalSyntax {
line_comment_prefixes: vec!["--".to_owned()],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use crate::compiled_project::main::legacy_schema_error::legacy_schema_error;
use crate::compiled_project::main::opt_out_position::opt_out_position;
use crate::compiled_project::main::text_position::text_position;
use crate::compiled_project::models::TextPosition;
use crate::compiled_project::tests::helpers::legacy_schema_files;
use crate::compiled_project::tests::test_types::{LegacySchemaTestCase, OptOutPositionTestCase};
use crate::compiled_project::tests::test_types::OptOutPositionTestCase;

#[test]
fn given_text_when_locating_opt_out_then_first_whole_word_key_after_start_is_found() {
Expand Down Expand Up @@ -38,50 +36,6 @@ fn given_text_when_locating_opt_out_then_first_whole_word_key_after_start_is_fou
}
}

#[test]
fn given_legacy_schema_files_when_matching_model_then_attachment_error_is_returned() {
let test_cases = [
LegacySchemaTestCase {
description: "schema file in another directory",
schema_files: &[("models/staging/schema.yml", &["orders"])],
expected_error: None,
},
LegacySchemaTestCase {
description: "root schema file declares the model",
schema_files: &[("schema.yml", &["orders"])],
expected_error: Some(
"Schema file schema.yml declares model 'orders', but model metadata must live in \
models/marts/orders.sql MODEL(...). Move description, columns, and audits into \
the model header.",
),
},
LegacySchemaTestCase {
description: "two schema files declare the model",
schema_files: &[
("models/schema.yml", &["orders"]),
("models/marts/schema.yml", &["orders", "customers"]),
],
expected_error: Some(
"Model file models/marts/orders.sql matched multiple schema.yml declarations: \
models/schema.yml, models/marts/schema.yml",
),
},
];

for test_case in test_cases {
assert_eq!(
legacy_schema_error(
("orders", "models/marts/orders.sql"),
&legacy_schema_files(test_case.schema_files)
)
.as_deref(),
test_case.expected_error,
"{}",
test_case.description
);
}
}

#[test]
fn given_character_offset_when_positioning_then_line_and_column_count_characters() {
let test_cases = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,6 @@ pub(super) struct OptOutPositionTestCase {
pub(super) expected_position: crate::compiled_project::models::TextPosition,
}

pub(super) struct LegacySchemaTestCase {
pub(super) description: &'static str,
/// `(schema file, model names)` per legacy schema file.
pub(super) schema_files: &'static [(&'static str, &'static [&'static str])],
pub(super) expected_error: Option<&'static str>,
}

pub(super) struct LocatedScanErrorTestCase {
pub(super) description: &'static str,
pub(super) sql: &'static str,
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod graph;
pub mod lineage;
pub mod query_analysis;
pub mod relation_names;
pub mod required_columns;
pub mod resource_diagnostics;
pub mod semantic_checks;
pub mod semantic_usage;
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlbuild-analysis/src/required_columns/_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub(crate) mod payload;
pub(crate) mod queries;
pub(crate) mod relations;
pub(crate) mod tree;
66 changes: 66 additions & 0 deletions crates/sqlbuild-analysis/src/required_columns/_helpers/payload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Expression payloads as the Polyglot wheel's `args` exposed them, and walks over them.

use polyglot_sql::{Expression, ast_json};
use serde_json::{Map, Value};

use crate::required_columns::_helpers::tree::payload;
use crate::required_columns::constants::{
KIND_COLUMN, KIND_SELECT, KIND_SUBQUERY, SET_OPERATION_KINDS,
};

/// `expression.args.get(key)`, unless the wheel exposed it as expression objects.
pub(crate) fn arg(expression: &Expression, key: &str) -> Option<Value> {
plain_value(payload(expression).remove(key))
}

/// `value`, unless the wheel exposed it as an expression or a list of expressions.
pub(crate) fn plain_value(value: Option<Value>) -> Option<Value> {
let value: Value = value?;
ast_json::expression_from_value(value.clone()).err()?;
ast_json::expressions_from_value(value.clone()).err()?;
Some(value)
}

/// `_payload_name`: a payload mapping's string `name`, else empty.
pub(crate) fn payload_name(payload: Option<&Value>) -> String {
payload
.and_then(Value::as_object)
.and_then(|map| map.get("name"))
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned()
}

/// `_column_refs_in_payload`: each column outside nested queries, in the wheel's sorted key order.
pub(crate) fn column_refs_in_payload(payload: Option<&Value>) -> Vec<(String, String)> {
match payload {
Some(Value::Array(items)) => items
.iter()
.flat_map(|item| column_refs_in_payload(Some(item)))
.collect(),
Some(Value::Object(map)) => object_column_refs(map),
_ => Vec::new(),
}
}

fn object_column_refs(map: &Map<String, Value>) -> Vec<(String, String)> {
let nested_query: bool = map.contains_key(KIND_SUBQUERY)
|| map.contains_key(KIND_SELECT)
|| SET_OPERATION_KINDS
.iter()
.any(|kind| map.contains_key(*kind));
if nested_query {
return Vec::new();
}
if let Some(Value::Object(column)) = map.get(KIND_COLUMN) {
return vec![(
payload_name(column.get("name")),
payload_name(column.get("table")),
)];
}
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
keys.into_iter()
.flat_map(|key| column_refs_in_payload(map.get(key)))
.collect()
}
Loading
Loading