-
Notifications
You must be signed in to change notification settings - Fork 229
Adds a SqlGlot implementation of the OSI SQL dialect #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
willpugh
wants to merge
7
commits into
main
Choose a base branch
from
willpugh/expression-language-sqlglot
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8516a0c
Adds a SqlGlot implementation of the OSI SQL dialect
willpugh d218df6
Add headers
willpugh d770be2
Add apache header to py.typed
willpugh da32a0c
Revert models modernization
willpugh ff64b14
Move SqlGlot dialect to core/python as a standalone package
willpugh 7a9b372
Minimize core-spec diff to the substantive precedence edits
willpugh 5ddda46
Drop dev-dependency-group change to python model package
willpugh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| repos: | ||
| - repo: local | ||
| hooks: | ||
| - id: ossie-sql-ruff-check | ||
| name: ossie-sql ruff check | ||
| entry: bash -c 'cd core/python && uv run ruff check --fix src tests' | ||
| language: system | ||
| files: ^core/python/.*\.py$ | ||
| pass_filenames: false | ||
| - id: ossie-sql-ruff-format | ||
| name: ossie-sql ruff format | ||
| entry: bash -c 'cd core/python && uv run ruff format src tests' | ||
| language: system | ||
| files: ^core/python/.*\.py$ | ||
| pass_filenames: false | ||
| - id: ossie-sql-mypy | ||
| name: ossie-sql mypy (strict) | ||
| entry: bash -c 'cd core/python && uv run mypy --config-file ../../mypy.ini src tests' | ||
| language: system | ||
| files: ^core/python/.*\.py$ | ||
| pass_filenames: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| .PHONY: format lint typecheck test | ||
|
|
||
| format: | ||
| uv run ruff format src tests | ||
|
|
||
| lint: | ||
| uv run ruff check src tests | ||
|
|
||
| typecheck: | ||
| uv run mypy --config-file ../../mypy.ini src tests | ||
|
|
||
| test: | ||
| uv run pytest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # apache-ossie-sql | ||
|
|
||
| A [SQLGlot](https://github.com/tobymao/sqlglot) dialect implementing the | ||
| Ossie expression language defined in | ||
| [`core-spec/expression_language.md`](../../core-spec/expression_language.md) | ||
| ("Ossie_SQL_2026"). | ||
|
|
||
| This package covers only the expression grammar: a custom SQLGlot `Dialect` | ||
| (tokenizer/parser/generator) so `sqlglot.parse_one(sql, read="ossie")` parses | ||
| and round-trips the spec's SQL subset (aggregate/window/date/string/math/ | ||
| conditional functions, typed literals, `CASE`, `CAST`/`TRY_CAST`, etc.), plus | ||
| a validator that rejects the constructs the spec explicitly disallows | ||
| (`SELECT`/`FROM`/`JOIN`, `GROUP BY`, `WHERE`, subqueries, CTEs, set | ||
| operations, DDL/DML). Wiring the dialect into the Ossie YAML model (the | ||
| spec's "Changes to YAML" section) is out of scope here. | ||
|
|
||
| ## Development | ||
|
|
||
| This package uses [`uv`](https://docs.astral.sh/uv/) for dependency | ||
| management. | ||
|
|
||
| ```bash | ||
| uv sync | ||
|
|
||
| # Run the test suite | ||
| uv run pytest | ||
|
|
||
| # Format code (auto-fixes in place) | ||
| uv run ruff format src tests | ||
|
|
||
| # Check formatting without modifying files | ||
| uv run ruff format --check src tests | ||
|
|
||
| # Lint | ||
| uv run ruff check src tests | ||
|
|
||
| # Type-check (strict; rules come from the shared repo-root mypy.ini) | ||
| uv run mypy --config-file ../../mypy.ini src tests | ||
| ``` | ||
|
|
||
| Or via the `Makefile`: | ||
|
|
||
| ```bash | ||
| make format | ||
| make lint | ||
| make typecheck | ||
| make test | ||
| ``` | ||
|
|
||
| ### Enforcing formatting/lint/type-checking locally | ||
|
|
||
| Install the pre-commit hooks scoped to this package so `ruff format`, | ||
| `ruff check`, and `mypy` run automatically before each commit: | ||
|
|
||
| ```bash | ||
| uv run pre-commit install -c core/python/.pre-commit-config.yaml | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| name = "apache-ossie-sql" | ||
| version = "0.1.0" | ||
| description = "SQLGlot dialect for the Ossie expression language (core-spec/expression_language.md)" | ||
| license = { text = "Apache-2.0" } | ||
| readme = "README.md" | ||
| requires-python = ">=3.11" | ||
| dependencies = [ | ||
| "sqlglot>=25.0", | ||
| ] | ||
|
|
||
| [dependency-groups] | ||
| dev = [ | ||
| "pytest>=8.0", | ||
| "mypy>=1.10", | ||
| "ruff>=0.11", | ||
| ] | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["src/ossie_sql"] | ||
|
|
||
| [tool.ruff] | ||
| extend = "../../ruff.toml" | ||
|
|
||
| [tool.pytest.ini_options] | ||
| testpaths = ["tests"] | ||
| pythonpath = ["src"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| """SQLGlot dialect and validation helpers for the Ossie expression language. | ||
|
|
||
| See ``core-spec/expression_language.md`` for the language spec this package | ||
| implements, and this package's README for scope and usage. | ||
| """ | ||
|
|
||
| from ossie_sql.dialect import Ossie | ||
| from ossie_sql.functions import ComplianceLevel, compliance_level | ||
| from ossie_sql.identifiers import ( | ||
| InvalidIdentifierError, | ||
| identifiers_equal, | ||
| normalize_identifier, | ||
| normalize_identifier_text, | ||
| ) | ||
| from ossie_sql.validate import UnsupportedConstructError, validate_expression | ||
|
|
||
| __all__ = [ | ||
| "ComplianceLevel", | ||
| "InvalidIdentifierError", | ||
| "Ossie", | ||
| "UnsupportedConstructError", | ||
| "compliance_level", | ||
| "identifiers_equal", | ||
| "normalize_identifier", | ||
| "normalize_identifier_text", | ||
| "validate_expression", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| """SQLGlot dialect for the Ossie expression language. | ||
|
|
||
| Implements the grammar defined in ``core-spec/expression_language.md`` | ||
| ("Ossie_SQL_2026"). Registers with SQLGlot as ``"ossie"``, so | ||
| ``sqlglot.parse_one(sql, read="ossie")`` parses the spec's SQL subset and | ||
| ``expression.sql(dialect="ossie")`` renders it back. | ||
|
|
||
| The spec is explicitly an ANSI SQL:2003 subset, so this dialect starts from | ||
| SQLGlot's default (ANSI-like) Tokenizer/Parser/Generator and only overrides | ||
| the handful of spec constructs that the default dialect either can't parse | ||
| in the spec's exact shape (``DATEADD``/``DATEDIFF``/``DATE_PART`` with a | ||
| bare, leading date-part argument) or renders back in a spelling the spec | ||
| doesn't define (e.g. ``STR_POSITION`` instead of ``POSITION ... IN``, | ||
| ``APPROX_DISTINCT`` instead of ``APPROX_COUNT_DISTINCT``). Functions the spec | ||
| lists that SQLGlot has no dedicated AST node for (``IFF``, ``ZEROIFNULL``, | ||
| ``NULLIFZERO``, ...) already round-trip correctly as-is via SQLGlot's generic | ||
| ``exp.Anonymous`` fallback and need no customization here. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable, Sequence | ||
| from typing import ClassVar | ||
|
|
||
| from sqlglot import TokenType, exp | ||
| from sqlglot.dialects.dialect import Dialect, unit_to_var | ||
| from sqlglot.generator import Generator | ||
| from sqlglot.helper import seq_get | ||
| from sqlglot.parser import Parser | ||
| from sqlglot.tokens import Tokenizer | ||
|
|
||
| _FuncBuilder = Callable[[Sequence[exp.Expression]], exp.Expression] | ||
|
|
||
|
|
||
| def _build_date_delta(exp_class: type[exp.DateAdd] | type[exp.DateDiff]) -> _FuncBuilder: | ||
| """Build a parser for ``FUNC(part, amount_or_start, date_or_end)``. | ||
|
|
||
| The spec puts the date-part identifier FIRST (``DATEADD(day, 7, d)``, | ||
| ``DATEDIFF(day, d1, d2)``). SQLGlot's generic positional ``Func`` | ||
| construction instead treats the *last* argument as the unit, so this | ||
| needs a dedicated builder rather than relying on the default mapping. | ||
| """ | ||
|
|
||
| def _builder(args: Sequence[exp.Expression]) -> exp.Expression: | ||
| return exp_class(this=seq_get(args, 2), expression=seq_get(args, 1), unit=seq_get(args, 0)) | ||
|
|
||
| return _builder | ||
|
|
||
|
|
||
| def _build_date_part(args: Sequence[exp.Expression]) -> exp.Expression: | ||
| """``DATE_PART(part, expr)`` is the spec's alternative spelling of | ||
| ``EXTRACT(part FROM expr)``; unify both into the same ``exp.Extract`` | ||
| node so callers see one canonical AST shape regardless of which | ||
| surface syntax was used. | ||
|
|
||
| ``DATE_PART`` takes its part name as a quoted string (``'year'``) while | ||
| ``EXTRACT`` takes a bare keyword (``YEAR``); normalize to the latter so | ||
| the merged AST always renders as valid ``EXTRACT(... FROM ...)`` syntax. | ||
| """ | ||
| part = seq_get(args, 0) | ||
| part_name = part.name if part is not None else "" | ||
| return exp.Extract(this=exp.var(part_name.upper()), expression=seq_get(args, 1)) | ||
|
|
||
|
|
||
| def _build_dpipe( | ||
| *, this: exp.Expression | None = None, expression: exp.Expression | None = None | ||
| ) -> exp.DPipe: | ||
| """Give ``||`` the same precedence tier as binary ``+``/``-``. | ||
|
|
||
| SQLGlot's default parser groups ``||`` with the bitwise operators, one | ||
| tier looser than ``+``/``-``. The spec instead follows the common convention, | ||
| which both put ``||`` at the *same* tier as ``+``/``-`` | ||
| (left-to-right, like the arithmetic operators around it) -- see | ||
| ``_OssieParser.TERM`` below, which is what actually makes that happen; | ||
| this just reproduces the ``safe=True`` default SQLGlot's own DPipe | ||
| construction uses so behavior is otherwise unchanged. | ||
| """ | ||
| return exp.DPipe(this=this, expression=expression, safe=True) | ||
|
|
||
|
|
||
| class _OssieTokenizer(Tokenizer): | ||
| pass | ||
|
|
||
|
|
||
| class _OssieParser(Parser): | ||
| FUNCTIONS = { | ||
| **Parser.FUNCTIONS, | ||
| "DATEADD": _build_date_delta(exp.DateAdd), | ||
| "DATEDIFF": _build_date_delta(exp.DateDiff), | ||
| "DATE_PART": _build_date_part, | ||
| "APPROX_PERCENTILE": exp.ApproxQuantile.from_arg_list, | ||
| } | ||
| # SQLGlot infers Parser.TERM's type from its literal (dict[TokenType, | ||
| # type[Binary]]), so mypy sees adding a plain builder function as an | ||
| # incompatible override -- it isn't, at runtime _parse_term only ever | ||
| # calls `klass(this=..., expression=...)` generically. Silence the two | ||
| # resulting checks rather than fight SQLGlot's own inferred type. | ||
| TERM: ClassVar[dict[TokenType, Callable[..., exp.Expression]]] = { # type: ignore[assignment] | ||
| **Parser.TERM, # type: ignore[dict-item] | ||
| TokenType.DPIPE: _build_dpipe, | ||
| } | ||
|
|
||
|
|
||
| class _OssieGenerator(Generator): | ||
| TYPE_MAPPING = { | ||
| **Generator.TYPE_MAPPING, | ||
| exp.DataType.Type.TIMESTAMPNTZ: "TIMESTAMP_NTZ", | ||
| } | ||
|
|
||
| def dateadd_sql(self, expression: exp.DateAdd) -> str: | ||
| return self.func("DATEADD", unit_to_var(expression), expression.expression, expression.this) | ||
|
|
||
| def datediff_sql(self, expression: exp.DateDiff) -> str: | ||
| return self.func( | ||
| "DATEDIFF", unit_to_var(expression), expression.expression, expression.this | ||
| ) | ||
|
|
||
| def strposition_sql(self, expression: exp.StrPosition) -> str: | ||
| this = self.sql(expression, "this") | ||
| substr = self.sql(expression, "substr") | ||
| return f"POSITION({substr} IN {this})" | ||
|
|
||
| def startswith_sql(self, expression: exp.StartsWith) -> str: | ||
| return self.func("STARTSWITH", expression.this, expression.expression) | ||
|
|
||
| def endswith_sql(self, expression: exp.EndsWith) -> str: | ||
| return self.func("ENDSWITH", expression.this, expression.expression) | ||
|
|
||
| def approxdistinct_sql(self, expression: exp.ApproxDistinct) -> str: | ||
| return self.func("APPROX_COUNT_DISTINCT", expression.this, expression.args.get("accuracy")) | ||
|
|
||
| def variancepop_sql(self, expression: exp.VariancePop) -> str: | ||
| # SQLGlot's own canonical spelling ("VARIANCE_POP") isn't a name the | ||
| # spec recognizes at all; the spec only defines "VAR_POP". | ||
| return self.func("VAR_POP", expression.this) | ||
|
|
||
| def dayofyear_sql(self, expression: exp.DayOfYear) -> str: | ||
| # Ditto: the spec defines "DAYOFYEAR", not SQLGlot's "DAY_OF_YEAR". | ||
| return self.func("DAYOFYEAR", expression.this) | ||
|
|
||
| def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str: | ||
| # The spec calls this "APPROX_PERCENTILE"; SQLGlot's canonical name | ||
| # ("APPROX_QUANTILE") isn't a spelling the spec defines. | ||
| return self.func("APPROX_PERCENTILE", expression.this, expression.args.get("quantile")) | ||
|
|
||
| def not_sql(self, expression: exp.Not) -> str: | ||
| # Render the compact `NOT IN`/`IS NOT` forms the spec documents, | ||
| # instead of SQLGlot's generic `NOT (x IN (...))` / `NOT (x IS NULL)`. | ||
| this = expression.this | ||
| if isinstance(this, exp.In): | ||
| return self.in_sql(this).replace(" IN ", " NOT IN ", 1) | ||
| if isinstance(this, exp.Is): | ||
| return self.binary(this, "IS NOT") | ||
| return super().not_sql(expression) | ||
|
|
||
| def tochar_sql(self, expression: exp.ToChar) -> str: | ||
| return self.func("TO_CHAR", expression.this, expression.args.get("format")) | ||
|
|
||
| def cast_sql(self, expression: exp.Cast, safe_prefix: str | None = None) -> str: | ||
| # Prefer the compact typed-literal form (`DATE '...'`) for a string | ||
| # literal cast to DATE/TIME/TIMESTAMP/TIMESTAMP_NTZ -- the spec's | ||
| # primary documented construction syntax for these types. | ||
| to = expression.to | ||
| this = expression.this | ||
| if ( | ||
| not safe_prefix | ||
| and isinstance(this, exp.Literal) | ||
| and this.is_string | ||
| and to.is_type( | ||
| exp.DataType.Type.DATE, | ||
| exp.DataType.Type.TIME, | ||
| exp.DataType.Type.TIMESTAMP, | ||
| exp.DataType.Type.TIMESTAMPNTZ, | ||
| ) | ||
| ): | ||
| return f"{self.sql(to)} {self.sql(this)}" | ||
| return super().cast_sql(expression, safe_prefix=safe_prefix) | ||
|
|
||
|
|
||
| class Ossie(Dialect): | ||
| """The Ossie_SQL_2026 expression dialect.""" | ||
|
|
||
| Tokenizer = _OssieTokenizer | ||
| Parser = _OssieParser | ||
| Generator = _OssieGenerator | ||
|
|
||
|
|
||
| __all__ = ["Ossie"] | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dialect name in the json spec and in the consumers should then just be "ossie" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to update the json spec, etc. as a different PR. I'm trying to keep this one to just the SqlGlot dialect.