Skip to content

Commit 3fb32ee

Browse files
authored
feat(native): extract model references natively behind the preview engine (#1008)
* feat(native): add the general SQL reference scan with table-function call arguments Port Python's general reference extraction to sqlbuild-sqltext: every reference kind including __table_fn with its call-argument count, under each adapter's lexical rules, with Python's exact error messages for argument counts, empty arguments, malformed names and unclosed text. A non-ASCII character that decides the outcome, or an unsupported line comment prefix, defers the text to Python. * feat(native): extract model references natively behind the preview engine Register reference extraction as a preview-tier native stage. Under native-preview every SQL text goes through the native scan, for every adapter's lexical rules; a deferral runs the Python scanner. A native error stands only when Python raises the same message, which is re-raised from Python; otherwise the compile fails with a NativeStageMismatchError instead of silently falling back. The python and native engines are unchanged. The lexical-syntax mapping for native scanners is shared through SqlLexicalSyntax.native_mapping and one binding input type. * feat(native): compare reference extraction in the compiler differential The failure corpus gains reference cases: wrong argument counts, an empty argument, an expression name, unclosed calls, and table functions without an argument list, with a single-quoted name or with an empty call argument. A harness test compares a project with table functions and references hidden in comments, strings and dollar quotes under both engines.
1 parent 7203112 commit 3fb32ee

38 files changed

Lines changed: 1663 additions & 38 deletions

File tree

‎crates/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ that can be tested and benchmarked with `cargo test` alone.
77
| Crate | Responsibility |
88
|---|---|
99
| `sqlbuild-core` | Constants shared by several crates, the panic boundary that turns an unwinding native failure into a named compiler error, passive identity, diagnostic and SQL value types, text positions, and a JSON emitter whose output matches Python's `json.dumps` and `orjson` byte for byte. |
10-
| `sqlbuild-sqltext` | Lexical SQL text without polyglot: comment, quote and parenthesis scanning, the rules quote policy, model header tokenization and matching, static variable substitution and static reference extraction. |
10+
| `sqlbuild-sqltext` | Lexical SQL text without polyglot: comment, quote and parenthesis scanning, the rules quote policy, model header tokenization and matching, static variable substitution, static reference extraction, and the general reference scan with table-function call arguments under each adapter's lexical rules. |
1111
| `sqlbuild-config` | Configuration file reading without Python: `tomllib`-compatible TOML, PyYAML `safe_load`-compatible YAML 1.1, and typed project and local config readers for the fields discovery needs. Errors carry their kind and position; YAML outside the supported subset is an `Unsupported` error. |
1212
| `sqlbuild-discovery` | Native project discovery and the only implementation of the model, SQL test, scenario, source and schema file collections and the declaration layout: the shared directory walk with Python's glob and sort semantics, file reading with Python's newline handling and read errors, and parsing of authored files with Python's exact discovery messages. Results are plain data the Python discovery facade materialises. |
1313
| `sqlbuild-scopes` | Declaration scopes: the scope index with Python's record orders and diagnostics, declaration visibility, relationship grants, the scope lookup groups, and the dialect-aware scan of SQL tests and scenarios for their expected models. Anything it cannot reproduce exactly defers to Python. |

‎crates/sqlbuild-python/src/bindings/_helpers/scopes/relationship_names.rs‎

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,15 @@
11
//! Expected-model relationship names of SQL tests and scenarios, scanned natively.
22
33
use pyo3::prelude::{Bound, PyModule, PyModuleMethods, PyResult, Python};
4-
use pyo3::{FromPyObject, pyfunction, wrap_pyfunction};
4+
use pyo3::{pyfunction, wrap_pyfunction};
55
use sqlbuild_scopes::relationship_names::main::expected_model_names::expected_model_names;
66
use sqlbuild_scopes::relationship_names::models::ExpectedNames;
77
use sqlbuild_sqltext::sql_scan::models::LexicalSyntax;
88

99
use crate::bindings::_helpers::boundary::panics::compiler_error;
10+
use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput;
1011
use crate::bindings::types::CompilerDetach;
1112

12-
/// One adapter's `SqlLexicalSyntax`, read from a Python mapping.
13-
#[derive(FromPyObject)]
14-
#[pyo3(from_item_all)]
15-
struct LexicalSyntaxInput {
16-
backslash_escape_quotes: Vec<String>,
17-
escape_string_prefix: bool,
18-
raw_string_prefix: bool,
19-
triple_quoted_strings: bool,
20-
nested_block_comments: bool,
21-
line_comment_prefixes: Vec<String>,
22-
}
23-
24-
impl From<LexicalSyntaxInput> for LexicalSyntax {
25-
fn from(input: LexicalSyntaxInput) -> Self {
26-
Self {
27-
backslash_escape_quotes: input.backslash_escape_quotes,
28-
escape_string_prefix: input.escape_string_prefix,
29-
raw_string_prefix: input.raw_string_prefix,
30-
triple_quoted_strings: input.triple_quoted_strings,
31-
nested_block_comments: input.nested_block_comments,
32-
line_comment_prefixes: input.line_comment_prefixes,
33-
}
34-
}
35-
}
36-
3713
/// Return each body's expected-model names, or `None` where Python must extract them.
3814
#[pyfunction]
3915
fn scope_expected_model_names(
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//! One adapter's `SqlLexicalSyntax`, read from a Python mapping.
2+
3+
use pyo3::FromPyObject;
4+
use sqlbuild_sqltext::sql_scan::models::LexicalSyntax;
5+
6+
/// One adapter's `SqlLexicalSyntax`, read from a Python mapping.
7+
#[derive(FromPyObject)]
8+
#[pyo3(from_item_all)]
9+
pub(crate) struct LexicalSyntaxInput {
10+
backslash_escape_quotes: Vec<String>,
11+
escape_string_prefix: bool,
12+
raw_string_prefix: bool,
13+
triple_quoted_strings: bool,
14+
nested_block_comments: bool,
15+
line_comment_prefixes: Vec<String>,
16+
}
17+
18+
impl From<LexicalSyntaxInput> for LexicalSyntax {
19+
fn from(input: LexicalSyntaxInput) -> Self {
20+
Self {
21+
backslash_escape_quotes: input.backslash_escape_quotes,
22+
escape_string_prefix: input.escape_string_prefix,
23+
raw_string_prefix: input.raw_string_prefix,
24+
triple_quoted_strings: input.triple_quoted_strings,
25+
nested_block_comments: input.nested_block_comments,
26+
line_comment_prefixes: input.line_comment_prefixes,
27+
}
28+
}
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
pub(crate) mod authored_values;
2+
pub(crate) mod lexical_syntax;
23
pub(crate) mod model_headers;
4+
pub(crate) mod registration;
5+
pub(crate) mod sql_references;
36
pub(crate) mod static_sql;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//! Register the lexical SQL text functions and classes.
2+
3+
use pyo3::prelude::{Bound, PyModule, PyResult};
4+
5+
use crate::bindings::_helpers::sqltext::{model_headers, sql_references, static_sql};
6+
7+
pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
8+
model_headers::register(module)?;
9+
static_sql::register(module)?;
10+
sql_references::register(module)?;
11+
Ok(())
12+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! Logical SQL references extracted natively under one adapter's lexical rules.
2+
3+
use pyo3::prelude::{Bound, PyModule, PyModuleMethods, PyResult};
4+
use pyo3::{pyclass, pymethods};
5+
use sqlbuild_sqltext::sql_references::main::extract_sql_references::extract_sql_references;
6+
use sqlbuild_sqltext::sql_references::models::{ReferenceExtraction, SqlReference};
7+
use sqlbuild_sqltext::sql_scan::models::LexicalSyntax;
8+
9+
use crate::bindings::_helpers::boundary::panics::compiler_guard;
10+
use crate::bindings::_helpers::sqltext::lexical_syntax::LexicalSyntaxInput;
11+
12+
/// `(kind, name, package, call_argument_count)` for one reference.
13+
type ReferenceRow = (&'static str, String, Option<String>, Option<usize>);
14+
/// The references, or Python's error message; `None` defers the text to Python.
15+
type ExtractionRow = Option<(Option<Vec<ReferenceRow>>, Option<String>)>;
16+
17+
/// Extracts references under one adapter's lexical rules, read once per syntax.
18+
#[pyclass(module = "sqlbuild._native", frozen)]
19+
pub(crate) struct SqlReferenceScanner {
20+
syntax: LexicalSyntax,
21+
}
22+
23+
#[pymethods]
24+
impl SqlReferenceScanner {
25+
#[new]
26+
fn new(syntax: LexicalSyntaxInput) -> Self {
27+
Self {
28+
syntax: syntax.into(),
29+
}
30+
}
31+
32+
/// Return `(references, None)`, `(None, message)` when Python raises, or `None` to defer.
33+
fn extract(&self, sql: &str) -> PyResult<ExtractionRow> {
34+
compiler_guard(|| {
35+
Ok(match extract_sql_references(sql, &self.syntax) {
36+
ReferenceExtraction::Extracted(references) => Some((
37+
Some(references.into_iter().map(reference_row).collect()),
38+
None,
39+
)),
40+
ReferenceExtraction::Failed(message) => Some((None, Some(message))),
41+
ReferenceExtraction::Deferred => None,
42+
})
43+
})
44+
}
45+
}
46+
47+
fn reference_row(reference: SqlReference) -> ReferenceRow {
48+
(
49+
reference.kind,
50+
reference.name,
51+
reference.package,
52+
reference.call_argument_count,
53+
)
54+
}
55+
56+
pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
57+
module.add_class::<SqlReferenceScanner>()?;
58+
Ok(())
59+
}

‎crates/sqlbuild-python/src/bindings/main/register.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ pub fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> {
2424
analysis::normalization::register(module)?;
2525
analysis::queries::register(module)?;
2626
analysis::sql_tests::register(module)?;
27-
sqltext::model_headers::register(module)?;
28-
sqltext::static_sql::register(module)?;
27+
sqltext::registration::register(module)?;
2928
boundary::oracles::register(module)?;
3029
discovery::project_files::register(module)?;
3130
scopes::scope_index::register(module)?;

‎crates/sqlbuild-sqltext/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
pub mod compiler;
66
mod constants;
7+
pub mod sql_references;
78
pub mod sql_scan;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod scan;

0 commit comments

Comments
 (0)