Skip to content
Closed
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
33 changes: 21 additions & 12 deletions Cargo.lock

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

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ sqlbuild-sqltext = { path = "crates/sqlbuild-sqltext" }
sqlbuild-state = { path = "crates/sqlbuild-state" }
# SQL lint Rules, quality Rules and the formatter.
sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "cf79656d247277a30bc81ef39b6dee420e721221" }
# The lossless SQL tokenizer behind query fingerprints.
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "4d0c6bc56da0e44eb164601a2af673ae7998257d" }
sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "4d0c6bc56da0e44eb164601a2af673ae7998257d" }
# SQLQuack's lossless tokenizer behind query fingerprints and its parsers for warehouse types.
sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "98eefae657b57566a091e39f7cde992bee45eb6f" }
sqlquack-syntax-bigquery = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "98eefae657b57566a091e39f7cde992bee45eb6f" }
sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "98eefae657b57566a091e39f7cde992bee45eb6f" }
stacker = "0.1"
syn = { version = "2", features = ["full", "visit", "extra-traits"] }
subtle = "2"
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sqlbuild-scopes.workspace = true
sqlbuild-sqltext.workspace = true
sqlclean.workspace = true
sqlquack-syntax.workspace = true
sqlquack-syntax-bigquery.workspace = true
sqlquack-syntax-postgres.workspace = true

[lints]
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 @@ -51,4 +51,5 @@ pub mod sql_tokens;
pub mod sql_tree;
pub mod target_paths;
pub mod type_system;
pub mod warehouse_types;
pub mod worker_stack;
12 changes: 12 additions & 0 deletions crates/sqlbuild-analysis/src/warehouse_types/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Constants of warehouse type parsing.

/// The tree key of a cast.
pub(crate) const CAST_KEY: &str = "cast";
/// The tree key of a cast's target type.
pub(crate) const CAST_TARGET_KEY: &str = "to";
/// The dialect whose type names go through PostgreSQL's own type grammar.
pub(crate) const POSTGRES_DIALECT: &str = "postgres";
/// The dialect whose type names are parsed as a Snowflake cast's target.
pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake";
/// The dialect whose type names are parsed as a BigQuery cast's target.
pub(crate) const BIGQUERY_DIALECT: &str = "bigquery";
1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod parse_warehouse_type;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! `parse_warehouse_type`: a warehouse type name as SQLQuack's parser for its dialect reads it.

use serde_json::Value;
use sqlquack_syntax::tokens::models::Dialect;
use sqlquack_syntax::{Expression, ParseOptions, parse_strict};

use crate::warehouse_types::constants::{
BIGQUERY_DIALECT, CAST_KEY, CAST_TARGET_KEY, POSTGRES_DIALECT, SNOWFLAKE_DIALECT,
};

/// The type tree of `spelling` in `dialect`, None without a parser, or why the parser rejects it.
pub fn parse_warehouse_type(spelling: &str, dialect: &str) -> Result<Option<Value>, String> {
let cast: String = format!("SELECT CAST(NULL AS {spelling})");
let statements: Vec<Expression> = match dialect {
POSTGRES_DIALECT => {
let data_type = sqlquack_syntax_postgres::parse_type(spelling)
.map_err(|error| format!("{error:?}"))?;
return serde_json::to_value(data_type)
.map(Some)
.map_err(|error| error.to_string());
}
SNOWFLAKE_DIALECT => parse_strict(&cast, Dialect::Snowflake, &ParseOptions::default())
.map_err(|error| format!("{error:?}"))?,
BIGQUERY_DIALECT => {
sqlquack_syntax_bigquery::parse_strict(&cast)
.map_err(|error| format!("{error:?}"))?
.statements
}
_ => return Ok(None),
};
let tree: Value = serde_json::to_value(&statements).map_err(|error| error.to_string())?;
match (statements.len(), cast_targets(&tree).as_slice()) {
(1, [target]) => Ok(Some((*target).clone())),
_ => Err(format!("`{spelling}` is not a single type name")),
}
}

/// Every cast's target type in `tree`, in order.
fn cast_targets(tree: &Value) -> Vec<&Value> {
let mut targets: Vec<&Value> = Vec::new();
let mut pending: Vec<&Value> = vec![tree];
while let Some(node) = pending.pop() {
match node {
Value::Object(fields) => {
for (key, value) in fields.iter().rev() {
pending.push(value);
if key == CAST_KEY
&& let Some(target) = value.get(CAST_TARGET_KEY)
{
targets.push(target);
}
}
}
Value::Array(items) => pending.extend(items.iter().rev()),
_ => {}
}
}
targets
}
4 changes: 4 additions & 0 deletions crates/sqlbuild-analysis/src/warehouse_types/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Warehouse column type names as SQLQuack's parser for each warehouse dialect reads them.

pub(crate) mod constants;
pub mod main;
13 changes: 13 additions & 0 deletions crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,24 @@ fn query_fingerprint(py: Python<'_>, sql: &str, dialect: &str) -> PyResult<Strin
.map_err(value_error)
}

/// A warehouse type name's JSON type tree, None without a parser, `ValueError` on a panic.
#[pyfunction]
fn parse_warehouse_type(py: Python<'_>, spelling: &str, dialect: &str) -> PyResult<Option<String>> {
py.compiler_detach(|| {
sqlbuild_analysis::warehouse_types::main::parse_warehouse_type::parse_warehouse_type(
spelling, dialect,
)
.map(|tree| tree.as_ref().map(serde_json::Value::to_string))
})
.map_err(value_error)
}

pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
crate::bindings::_helpers::analysis::binding_catalog::register(module)?;
crate::bindings::_helpers::analysis::required_columns::register(module)?;
crate::bindings::_helpers::references::explicit_references::register(module)?;
crate::bindings::_helpers::analysis::binding_locations::register(module)?;
module.add_function(wrap_pyfunction!(query_fingerprint, module)?)?;
module.add_function(wrap_pyfunction!(parse_warehouse_type, module)?)?;
Ok(())
}
4 changes: 4 additions & 0 deletions fensu.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2932,3 +2932,7 @@ reason = "The override keeps queue.Queue.put's positional protocol, which every
rule = "FFS201"
path = "src/sqlbuild/orchestration_declarations/models/sensor_context.py"
reason = "Only SensorContext lives here: invocation-local ctx.cursor assignment proposes progress, committed solely by native fenced completion. The package regression guards this declaration-only scope; class-symbol exceptions are unsupported."

[[dupes.allowlist]]
paths = ["crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs", "crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs", "crates/sqlbuild-analysis/src/relation_names/_helpers/walk.rs"]
reason = "Each is a short stack walk over a JSON syntax tree with its own stop and collect rule: cast_targets collects every cast's target type, branch_selects stops at each set-operation branch, and walked lists every object. A shared visitor would need a callback for each rule and save no logic."
5 changes: 0 additions & 5 deletions python-boundary.toml
Original file line number Diff line number Diff line change
Expand Up @@ -567,11 +567,6 @@ path = "compiler/auditing/main/get_audit_factory_definition.py"
rule = 3
note = "Reads the declaration attached to a project audit factory."

[[stays]]
path = "compiler/sql_analysis/main/import_polyglot.py"
rule = 4
note = "Imports the Polyglot Python package for the Python API."

[[stays]]
path = "compiler/sql_analysis/main/import_polyglot_sql.py"
rule = 4
Expand Down
1 change: 1 addition & 0 deletions src/sqlbuild/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3458,6 +3458,7 @@ class PreparedCompileArtifacts:

def format_sql_json(request_json: str) -> str: ...
def query_fingerprint(sql: str, dialect: str) -> str: ...
def parse_warehouse_type(spelling: str, dialect: str) -> str | None: ...
def lint_sql_json(request_json: str) -> str: ...
def prepare_lint_sql(
request: LintPreparationRequest,
Expand Down
13 changes: 0 additions & 13 deletions src/sqlbuild/compiler/sql_analysis/main/import_polyglot.py

This file was deleted.

Loading
Loading