trino-sql-validator 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/AGENTS.md +14 -2
  2. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/CHANGELOG.md +14 -0
  3. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/Cargo.lock +13 -1
  4. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/Cargo.toml +2 -2
  5. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/PKG-INFO +18 -1
  6. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/README.md +17 -0
  7. trino_sql_validator-0.2.0/plan/functions-validation.md +109 -0
  8. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/plan/plan.md +5 -1
  9. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/plan/roadmap.md +6 -0
  10. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/pyproject.toml +1 -1
  11. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/python/trino_sql_validator/__init__.py +47 -7
  12. trino_sql_validator-0.2.0/python/trino_sql_validator/_native.pyi +15 -0
  13. trino_sql_validator-0.2.0/src/functions.rs +503 -0
  14. trino_sql_validator-0.2.0/src/lib.rs +262 -0
  15. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/test_validator.py +66 -1
  16. trino_sql_validator-0.2.0/tools/extract_functions.py +196 -0
  17. trino_sql_validator-0.1.0/python/trino_sql_validator/_native.pyi +0 -11
  18. trino_sql_validator-0.1.0/src/lib.rs +0 -159
  19. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/.github/workflows/ci.yml +0 -0
  20. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/.github/workflows/release.yml +0 -0
  21. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/.gitignore +0 -0
  22. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/LICENSE +0 -0
  23. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/python/trino_sql_validator/py.typed +0 -0
  24. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/rust-toolchain.toml +0 -0
  25. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/src/dialects/mod.rs +0 -0
  26. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/conftest.py +0 -0
  27. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/fixtures/ddl_multi.sql +0 -0
  28. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/fixtures/empty.sql +0 -0
  29. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/fixtures/invalid_one.sql +0 -0
  30. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/fixtures/trino_specific.sql +0 -0
  31. {trino_sql_validator-0.1.0 → trino_sql_validator-0.2.0}/tests/fixtures/valid_multi.sql +0 -0
@@ -10,10 +10,12 @@ Built with PyO3 + maturin. Distributed as wheels + sdist on PyPI.
10
10
  Layout:
11
11
  - `src/` — Rust crate (the compiled `_native` extension). `src/lib.rs` holds the
12
12
  `#[pymodule] fn _native`. `src/dialects/` holds the `TrinoDialect`/`SqlDialect`.
13
+ `src/functions.rs` is the **generated** Trino function catalog (`@generated`).
13
14
  - `python/trino_sql_validator/` — pure-Python public API (`__init__.py`), type
14
15
  stubs (`_native.pyi`), `py.typed`.
15
16
  - `tests/` — pytest suite for the public API (+ `.sql` fixtures).
16
- - `plan/` — planning docs (`plan.md`, `roadmap.md`).
17
+ - `plan/` — planning docs (`plan.md`, `roadmap.md`, `functions-validation.md`).
18
+ - `tools/extract_functions.py` — regenerates `src/functions.rs` from Trino docs.
17
19
 
18
20
  ## Source of truth
19
21
 
@@ -47,6 +49,10 @@ mypy python/trino_sql_validator
47
49
  # Produce distributable artifacts
48
50
  maturin build --release # wheels
49
51
  maturin sdist # source distribution
52
+
53
+ # Regenerate the Trino function catalog from upstream docs
54
+ python tools/extract_functions.py # fetch from GitHub
55
+ python tools/extract_functions.py --docs-path /path/to/trino/docs/src/main/sphinx/functions # local checkout
50
56
  ```
51
57
 
52
58
  ## Conventions / rules
@@ -86,4 +92,10 @@ maturin sdist # source distribution
86
92
  `sqlparser-rs` does syntax, not semantics. It accepts some SQL Trino rejects at
87
93
  semantic analysis and can reject exotic Trino-specific DDL. This is a documented,
88
94
  accepted limitation (see README + plan/roadmap.md). Do not "fix" by loosening the
89
- dialect to Generic by default for `dialect="trino"`.
95
+ dialect to Generic by default for `dialect="trino"`.
96
+
97
+ Function validation (`ValidationResult.warnings`) checks only **name existence**
98
+ against the documented Trino catalog (`src/functions.rs`); it does not check
99
+ arity or argument types — that is semantic analysis, out of scope for a syntax
100
+ validator. False positives are possible if a deployed Trino has plugin functions
101
+ beyond the docs; warnings are non-fatal by design.
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-09-06
9
+
10
+ ### Added
11
+
12
+ - Trino **function-name validation**: for `dialect="trino"`, `validate()` and
13
+ `validate_file()` now walk the parsed AST and flag calls to functions that are
14
+ not in the documented Trino catalog (e.g. a misspelled `round`).
15
+ - New `ValidationResult.warnings` (`FunctionWarning`) and convenience property
16
+ `ValidationResult.unknown_functions`. Unknown functions are non-fatal warnings
17
+ — `valid` stays `True` because the syntax is fine.
18
+ - `tools/extract_functions.py` — regenerates the embedded catalog
19
+ (`src/functions.rs`, 459 canonical names) from the Trino docs; committed so
20
+ builds stay offline and deterministic.
21
+
8
22
  ## [0.1.0] - 2026-09-06
9
23
 
10
24
  Initial release.
@@ -235,6 +235,18 @@ checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec"
235
235
  dependencies = [
236
236
  "log",
237
237
  "recursive",
238
+ "sqlparser_derive",
239
+ ]
240
+
241
+ [[package]]
242
+ name = "sqlparser_derive"
243
+ version = "0.5.0"
244
+ source = "registry+https://github.com/rust-lang/crates.io-index"
245
+ checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289"
246
+ dependencies = [
247
+ "proc-macro2",
248
+ "quote",
249
+ "syn",
238
250
  ]
239
251
 
240
252
  [[package]]
@@ -269,7 +281,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
269
281
 
270
282
  [[package]]
271
283
  name = "trino-sql-validator"
272
- version = "0.1.0"
284
+ version = "0.2.0"
273
285
  dependencies = [
274
286
  "pyo3",
275
287
  "regex",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "trino-sql-validator"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  description = "Fast Trino SQL syntax validator — Python library backed by Rust"
6
6
  license = "MIT"
@@ -16,7 +16,7 @@ crate-type = ["cdylib"]
16
16
 
17
17
  [dependencies]
18
18
  pyo3 = { version = "0.29", features = ["abi3-py310"] }
19
- sqlparser = { version = "0.62", default-features = true }
19
+ sqlparser = { version = "0.62", default-features = true, features = ["visitor"] }
20
20
  regex = "1"
21
21
 
22
22
  [dev-dependencies]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trino-sql-validator
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -60,6 +60,23 @@ Invalid SQL (and files containing it) is returned as a `ValidationResult`;
60
60
  it is **not** raised as an exception. Only genuine misuse (unknown dialect,
61
61
  unreadable file) raises.
62
62
 
63
+ ### Function-name warnings
64
+
65
+ For `dialect="trino"`, `validate()` also checks that every function called in
66
+ the SQL exists in the documented Trino catalog. Unknown names are reported as
67
+ non-fatal `warnings` — `valid` stays `True` because syntax is fine:
68
+
69
+ ```python
70
+ result = validate("SELECT marh(1.5)") # round() misspelled
71
+ assert result.valid
72
+ print(result.warnings) # (FunctionWarning(name='marh', line=1, column=8),)
73
+ print(result.unknown_functions) # ['marh']
74
+ ```
75
+
76
+ The catalog is auto-generated from the Trino docs and only checks *name
77
+ existence*, not argument count or types. `hive`/`generic` dialects skip this
78
+ check.
79
+
63
80
  ### Dialects
64
81
 
65
82
  - `"trino"` (default) — Trino-flavored with a custom override tuned for
@@ -33,6 +33,23 @@ Invalid SQL (and files containing it) is returned as a `ValidationResult`;
33
33
  it is **not** raised as an exception. Only genuine misuse (unknown dialect,
34
34
  unreadable file) raises.
35
35
 
36
+ ### Function-name warnings
37
+
38
+ For `dialect="trino"`, `validate()` also checks that every function called in
39
+ the SQL exists in the documented Trino catalog. Unknown names are reported as
40
+ non-fatal `warnings` — `valid` stays `True` because syntax is fine:
41
+
42
+ ```python
43
+ result = validate("SELECT marh(1.5)") # round() misspelled
44
+ assert result.valid
45
+ print(result.warnings) # (FunctionWarning(name='marh', line=1, column=8),)
46
+ print(result.unknown_functions) # ['marh']
47
+ ```
48
+
49
+ The catalog is auto-generated from the Trino docs and only checks *name
50
+ existence*, not argument count or types. `hive`/`generic` dialects skip this
51
+ check.
52
+
36
53
  ### Dialects
37
54
 
38
55
  - `"trino"` (default) — Trino-flavored with a custom override tuned for
@@ -0,0 +1,109 @@
1
+ # Function-name validation for Trino
2
+
3
+ Status: implemented for v0.2.0.
4
+ Source of truth for the catalog: Trino docs at
5
+ `docs/src/main/sphinx/functions/` in `trinodb/trino` (MyST, current format).
6
+
7
+ ## Goal
8
+
9
+ Catch a common class of error today's validator ships past: a syntactically
10
+ well-formed SQL statement that calls a function Trino does not provide (typo,
11
+ wrong engine, e.g. `SELECT marh(x)` vs `round(x)`). `sqlparser-rs` treats any
12
+ `name(...)` as a function call, so it never flags unknown names.
13
+
14
+ ## Where the catalog comes from
15
+
16
+ The `functions/` tree has 35 topic files (`aggregate.md`, `string.md`, ...) plus
17
+ `list.md`. A function name appears in two machine-greppable forms:
18
+
19
+ - `:::{function} name(args) -> type` directives (the authoritative signatures,
20
+ one per overload).
21
+ - `{func}\`name\`` cross-references in `list.md` (the alphabetical catalog of
22
+ every documented function).
23
+
24
+ Our catalog = **union** of directive names across all topic files AND `{func}`
25
+ names in `list.md`. Current Trino docs yield **459 canonical names**
26
+ (measured 2026-09-06). This catches all of `count/sum/avg/min/max/...` plus
27
+ Trino-only functions.
28
+
29
+ Cast/keywords that sqlparser models specially (`cast`, `try`, `if`, `coalesce`)
30
+ never reach the function walker, so they need no special-casing.
31
+
32
+ ## Algorithm (time-optimal)
33
+
34
+ 1. **No extra cost on invalid SQL.** Parse first. If `Parser::parse_sql` fails,
35
+ return the existing error today and skip function walking entirely.
36
+ 2. **Single linear AST walk.** For a successful parse, walk every statement once
37
+ with `sqlparser`'s `visit_expressions` (`feature = "visitor"`). For each
38
+ `Expr::Function`, look up the *last* `Ident` of its `ObjectName` in a static
39
+ `HashSet<&'static str>` of canonical names. O(functions-in-query), O(1) lookup.
40
+ 3. **Exact line/column for free.** `Ident.span` gives byte-exact start
41
+ `Location` — no regex, no string scanning (unlike the error-location path).
42
+ 4. **Static, lock-free data.** The name list is committed to `src/functions.rs`
43
+ as a `LazyLock<HashSet>`. Built once by `tools/extract_functions.py` (a
44
+ dev-only script that pulls the docs). No runtime/build-time network.
45
+
46
+ The name list only changes when Trino adds/removes functions (a release-scale
47
+ event). Committing the generated list keeps the crate offline-buildable and
48
+ deterministic; regenerating is opt-in (`python tools/extract_functions.py`).
49
+
50
+ ## Behavior / semantics
51
+
52
+ - Only `dialect == "trino"` checks functions. `hive`/`generic` stay as-is
53
+ (permissive escapes hatch; their function surface differs from Trino's).
54
+ - A call to an unknown function does **not** make the statement "invalid" in the
55
+ existing `valid: bool` sense — that stays strictly about syntax. It is
56
+ reported as a `warning` alongside the syntax result. This preserves the
57
+ "bad syntax is a value, not an exception" contract on parse errors and keeps
58
+ the truly-syntactic check peer-reviewable.
59
+ - Stopping early: the walker records unknown names and their locations and can
60
+ short-circuit (via `ControlFlow::Break`) to avoid scanning the rest of a large
61
+ script once already-warned — bounded memory, unchanged O(n) time.
62
+ - Fully-qualified calls (`schema.round(x)`): match on the bare function name;
63
+ the leading qualifier is a schema reference, not part of the function.
64
+
65
+ ## API shape
66
+
67
+ `ValidationResult` gains an optional `warnings: tuple[FunctionWarning, ...]`.
68
+
69
+ ```python
70
+ @dataclass(frozen=True)
71
+ class FunctionWarning:
72
+ name: str
73
+ line: int | None = None
74
+ column: int | None = None
75
+ ```
76
+
77
+ Rust `validate`/`validate_file` return a 6-tuple
78
+ `(valid, statement_count, error_message, line, column, warnings)` where
79
+ `warnings` is a list of `(name, line, column)` triples. pyo3 converts the
80
+ plain `Vec<(String, Option<usize>, Option<usize>)>` to a Python list of tuples
81
+ directly — no serialization dependency needed.
82
+
83
+ ## Files
84
+
85
+ - `tools/extract_functions.py` — dev-only doc parser → `src/functions.rs`.
86
+ - `src/functions.rs` — generated static catalog + `is_known_function()`.
87
+ - `src/lib.rs` — enable `visitor` feature usage; walk AST after successful parse.
88
+ - `python/trino_sql_validator/__init__.py` — `FunctionWarning`, `warnings` field.
89
+ - `python/trino_sql_validator/_native.pyi` — updated tuple type.
90
+ - `tests/test_validator.py` — unknown/known/qualified/trino-only cases.
91
+ - `tests/fixtures/` — sample queries with/without unknown functions.
92
+
93
+ ## Verification
94
+
95
+ - `cargo test` (Rust unit tests for the walker + catalog sanity).
96
+ - `pytest` — unknown function → warning with correct line/column; known &
97
+ multi-argument & qualified calls → no warning; `dialect="generic"` → no
98
+ function checks; invalid SQL → parse error, no warnings.
99
+ - `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `ruff`,
100
+ `mypy` all stay green.
101
+
102
+ ## Trade-offs / limits (documented, not "fixed" here)
103
+
104
+ - Catalog is the *documented* function set. Trino deploys may have
105
+ connector/plugin-specific functions beyond docs → possible false positives for
106
+ exotic setups. Warnings are non-fatal and per-dialect-trino, so this is safe.
107
+ - We validate name existence, not arity/type (sqlparser does not model Trino's
108
+ function signatures beyond parse; arity/type checking is semantic and out of
109
+ scope for a syntax validator). See roadmap for `trino-parser` on the far end.
@@ -43,12 +43,16 @@ trino-sql-validator/
43
43
  ├── AGENTS.md
44
44
  ├── plan/
45
45
  │ ├── plan.md # this file
46
- │ └── roadmap.md # future work (Trino-exact grammar, etc.)
46
+ │ ├── roadmap.md # future work (Trino-exact grammar, etc.)
47
+ │ └── functions-validation.md # function-name validation plan (v0.2.0)
47
48
  ├── src/
48
49
  │ ├── lib.rs # #[pymodule] entry point
50
+ │ ├── functions.rs # GENERATED Trino function catalog (459 names)
49
51
  │ ├── dialects/
50
52
  │ │ └── mod.rs # TrinoDialect (impl Dialect trait)
51
53
  │ └── error.rs # Mapping ParserError -> Python exception info
54
+ ├── tools/
55
+ │ └── extract_functions.py # regenerates src/functions.rs from Trino docs
52
56
  ├── python/trino_sql_validator/
53
57
  │ ├── __init__.py # pure-Python public API (re-exports, __all__)
54
58
  │ ├── _validator.pyi # type stubs referencing the compiled _native module
@@ -6,6 +6,12 @@
6
6
  - Wheels for Linux (x86_64, aarch64), macOS (arm64, x86_64), Windows (x86_64).
7
7
  - PyPI publishing via GitHub Actions + Trusted Publishing.
8
8
 
9
+ ## v0.2.0
10
+ - Trino function-name validation: AST walk via `sqlparser::visitor`; unknown
11
+ functions are surfaced as non-fatal `FunctionWarning` in `ValidationResult.warnings`.
12
+ Catalog (459 names) auto-generated from Trino docs and committed in `src/functions.rs`.
13
+ Only active for `dialect="trino"`.
14
+
9
15
  ## v0.2.x — clearer Trino cursor
10
16
  - Enrich `TrinoDialect` overrides for commonly-mis-parsed Trino-specific syntax:
11
17
  - `EXECUTE IMMEDIATE`, `CALL` signatures
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "trino-sql-validator"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Fast Trino SQL syntax validator — Python library backed by Rust"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -15,15 +15,23 @@ from ._native import validate_file as _native_validate_file
15
15
 
16
16
  __all__ = [
17
17
  "Error",
18
+ "FunctionWarning",
18
19
  "ValidationResult",
19
20
  "__version__",
20
21
  "validate",
21
22
  "validate_file",
22
23
  ]
23
24
 
24
- _NativeResult = tuple[bool, int, str | None, int | None, int | None]
25
- """Tuple shape returned by the Rust core: (valid, statement_count, error,
26
- message, line, column)."""
25
+ _NativeResult = tuple[
26
+ bool,
27
+ int,
28
+ str | None,
29
+ int | None,
30
+ int | None,
31
+ tuple[tuple[str, int | None, int | None], ...],
32
+ ]
33
+ """Tuple shape returned by the Rust core: (valid, statement_count, error
34
+ message, line, column, warnings)."""
27
35
 
28
36
  __version__ = _native.__version__
29
37
 
@@ -44,6 +52,25 @@ class Error:
44
52
  return self.message
45
53
 
46
54
 
55
+ @dataclass(frozen=True)
56
+ class FunctionWarning:
57
+ """A call to a function that is not in the documented Trino catalog.
58
+
59
+ ``valid`` stays ``True`` for such statements — name checks are advisory,
60
+ not syntax errors (a deployed Trino may still offer plugin functions that
61
+ the docs do not list).
62
+ """
63
+
64
+ name: str
65
+ line: int | None = None
66
+ column: int | None = None
67
+
68
+ def __str__(self) -> str:
69
+ if self.line is not None and self.column is not None:
70
+ return f"unknown function '{self.name}' at line {self.line}, column {self.column}"
71
+ return f"unknown function '{self.name}'"
72
+
73
+
47
74
  @dataclass(frozen=True)
48
75
  class ValidationResult:
49
76
  """Structured outcome of validating one or more SQL statements."""
@@ -51,14 +78,26 @@ class ValidationResult:
51
78
  valid: bool
52
79
  statement_count: int
53
80
  error: Error | None = None
81
+ warnings: tuple[FunctionWarning, ...] = ()
82
+
83
+ @property
84
+ def unknown_functions(self) -> list[str]:
85
+ """Function names used in the SQL that have no Trino documentation
86
+ entry, in order of appearance."""
87
+ return [warning.name for warning in self.warnings]
54
88
 
55
89
  def __bool__(self) -> bool:
56
90
  return self.valid
57
91
 
58
92
  def __repr__(self) -> str:
59
- if self.valid:
60
- return f"<ValidationResult valid=True statements={self.statement_count}>"
61
- return f"<ValidationResult valid=False error={self.error!r}>"
93
+ if not self.valid:
94
+ return f"<ValidationResult valid=False error={self.error!r}>"
95
+ if self.warnings:
96
+ return (
97
+ f"<ValidationResult valid=True statements={self.statement_count} "
98
+ f"warnings={len(self.warnings)}>"
99
+ )
100
+ return f"<ValidationResult valid=True statements={self.statement_count}>"
62
101
 
63
102
 
64
103
  _SUPPORTED_DIALECTS = ("trino", "hive", "generic")
@@ -69,11 +108,12 @@ def _validate(dialect: Dialect, call: _NativeResult) -> ValidationResult:
69
108
  raise ValueError(
70
109
  f"unknown dialect {dialect!r}; expected one of {_SUPPORTED_DIALECTS}"
71
110
  )
72
- valid, statement_count, message, line, column = call
111
+ valid, statement_count, message, line, column, warnings = call
73
112
  return ValidationResult(
74
113
  valid=bool(valid),
75
114
  statement_count=int(statement_count),
76
115
  error=Error(message=message, line=line, column=column) if message else None,
116
+ warnings=tuple(FunctionWarning(name=name, line=wl, column=wc) for name, wl, wc in warnings),
77
117
  )
78
118
 
79
119
 
@@ -0,0 +1,15 @@
1
+ from typing import TypeAlias
2
+
3
+ __version__: str
4
+
5
+ _Warning: TypeAlias = tuple[str, int | None, int | None]
6
+
7
+ def validate(
8
+ sql: str,
9
+ dialect: str = "trino",
10
+ ) -> tuple[bool, int, str | None, int | None, int | None, tuple[_Warning, ...]]: ...
11
+
12
+ def validate_file(
13
+ path: str,
14
+ dialect: str = "trino",
15
+ ) -> tuple[bool, int, str | None, int | None, int | None, tuple[_Warning, ...]]: ...