sqlstratum 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antonio Ognio <aognio@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlstratum
3
+ Version: 0.1.0
4
+ Summary: Lightweight, source-first SQL AST + compiler + runner.
5
+ Author-email: Antonio Ognio <aognio@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Antonio Ognio <aognio@gmail.com>
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/aognio/sqlstratum
29
+ Project-URL: Repository, https://github.com/aognio/sqlstratum
30
+ Project-URL: Issues, https://github.com/aognio/sqlstratum/issues
31
+ Keywords: sql,query-builder,sqlite,compiler,ast
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3 :: Only
37
+ Classifier: Programming Language :: Python :: 3.8
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Database
44
+ Classifier: Topic :: Software Development :: Libraries
45
+ Requires-Python: >=3.8
46
+ Description-Content-Type: text/markdown
47
+ License-File: LICENSE
48
+ Provides-Extra: dev
49
+ Requires-Dist: build>=1.2.0; extra == "dev"
50
+ Requires-Dist: twine>=5.0.0; extra == "dev"
51
+ Dynamic: license-file
52
+
53
+ # SQLStratum
54
+
55
+ <p align="center">
56
+ <img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
57
+ </p>
58
+
59
+ SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
60
+ SQLite runner and a hydration pipeline. It exists to give applications and ORMs a reliable foundation
61
+ layer with composable SQL, predictable parameter binding, and explicit execution boundaries.
62
+
63
+ ## Key Features
64
+ - Deterministic compilation: identical AST inputs produce identical SQL + params
65
+ - Typed, composable DSL for SELECT/INSERT/UPDATE/DELETE
66
+ - Safe parameter binding (no raw interpolation)
67
+ - Hydration targets for structured results
68
+ - SQLite-first execution via a small Runner API
69
+ - Testable compiled output and runtime behavior
70
+
71
+ ## Non-Goals
72
+ - Not an ORM (no identity map, relationships, lazy loading)
73
+ - Not a migrations/DDL system
74
+ - Not a full database abstraction layer for every backend yet (SQLite first)
75
+ - Not a SQL string templating engine
76
+
77
+ SQLStratum focuses on queries. DDL statements such as `CREATE TABLE` or `ALTER TABLE` are intended to
78
+ live in a complementary library with similar design goals that is currently in the works.
79
+
80
+ ## Quickstart
81
+ ```python
82
+ import sqlite3
83
+
84
+ from sqlstratum import SELECT, INSERT, Table, col, Runner
85
+
86
+ users = Table(
87
+ "users",
88
+ col("id", int),
89
+ col("email", str),
90
+ col("active", int),
91
+ )
92
+
93
+ conn = sqlite3.connect(":memory:")
94
+ runner = Runner(conn)
95
+ runner.exec_ddl("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, active INTEGER)")
96
+
97
+ runner.execute(INSERT(users).VALUES(email="a@b.com", active=1))
98
+ runner.execute(INSERT(users).VALUES(email="c@d.com", active=0))
99
+
100
+ q = (
101
+ SELECT(users.c.id, users.c.email)
102
+ .FROM(users)
103
+ .WHERE(users.c.active.is_true())
104
+ .HYDRATE(dict)
105
+ )
106
+
107
+ rows = runner.fetch_all(q)
108
+ print(rows)
109
+ ```
110
+
111
+ ## Why `Table` objects?
112
+ SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
113
+ provides column metadata and a stable namespace for column access, which enables predictable SQL
114
+ generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
115
+ names in joins.
116
+
117
+ ## Project Structure
118
+ - AST: immutable query nodes in `sqlstratum/ast.py`
119
+ - Compiler: SQL + params generation in `sqlstratum/compile.py`
120
+ - Runner: SQLite execution and transactions in `sqlstratum/runner.py`
121
+ - Hydration: projection rules and targets in `sqlstratum/hydrate.py`
122
+
123
+ ## SQL Debugging
124
+ SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
125
+ intentionally gated to avoid noisy output in production. Debug output requires two conditions:
126
+ - Environment variable gate: `SQLSTRATUM_DEBUG` must be truthy (`"1"`, `"true"`, `"yes"`,
127
+ case-insensitive).
128
+ - Logger gate: the `sqlstratum` logger must be DEBUG-enabled.
129
+
130
+ Why it does not work by default: Python logging defaults to WARNING level, so even if
131
+ `SQLSTRATUM_DEBUG=1` is set, DEBUG logs will not appear unless logging is configured.
132
+
133
+ To enable debugging in a development app:
134
+
135
+ Step 1 - set the environment variable:
136
+ ```
137
+ SQLSTRATUM_DEBUG=1
138
+ ```
139
+
140
+ Step 2 - configure logging early in the app:
141
+ ```python
142
+ import logging
143
+
144
+ logging.basicConfig(level=logging.DEBUG)
145
+ # or
146
+ logging.getLogger("sqlstratum").setLevel(logging.DEBUG)
147
+ ```
148
+
149
+ Output looks like:
150
+ ```
151
+ SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
152
+ ```
153
+
154
+ Architectural intent: logging happens at the Runner boundary (after execution). AST building and
155
+ compilation remain deterministic and side-effect free, preserving separation of concerns.
156
+
157
+ ## Logo Inspiration
158
+
159
+ Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
160
+ Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/wiki/Vinicunca) for
161
+ background.
162
+
163
+ ## Versioning / Roadmap
164
+ Current version: `0.1.0`.
165
+ Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
166
+ minimal at this stage and will evolve with real usage.
167
+
168
+ ## Authorship
169
+ [Antonio Ognio](https://github.com/aognio/) is the maintainer and author of SQLStratum. ChatGPT is used for brainstorming,
170
+ architectural thinking, documentation drafting, and project management advisory. Codex (CLI/agentic
171
+ coding) is used to implement many code changes under Antonio's direction and review. The maintainer
172
+ reviews and curates changes; AI tools are assistants, not owners, and accountability remains with the
173
+ maintainer.
174
+
175
+ ## License
176
+ MIT License.
177
+
178
+ ## Contributing
179
+ PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
@@ -0,0 +1,127 @@
1
+ # SQLStratum
2
+
3
+ <p align="center">
4
+ <img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
5
+ </p>
6
+
7
+ SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
8
+ SQLite runner and a hydration pipeline. It exists to give applications and ORMs a reliable foundation
9
+ layer with composable SQL, predictable parameter binding, and explicit execution boundaries.
10
+
11
+ ## Key Features
12
+ - Deterministic compilation: identical AST inputs produce identical SQL + params
13
+ - Typed, composable DSL for SELECT/INSERT/UPDATE/DELETE
14
+ - Safe parameter binding (no raw interpolation)
15
+ - Hydration targets for structured results
16
+ - SQLite-first execution via a small Runner API
17
+ - Testable compiled output and runtime behavior
18
+
19
+ ## Non-Goals
20
+ - Not an ORM (no identity map, relationships, lazy loading)
21
+ - Not a migrations/DDL system
22
+ - Not a full database abstraction layer for every backend yet (SQLite first)
23
+ - Not a SQL string templating engine
24
+
25
+ SQLStratum focuses on queries. DDL statements such as `CREATE TABLE` or `ALTER TABLE` are intended to
26
+ live in a complementary library with similar design goals that is currently in the works.
27
+
28
+ ## Quickstart
29
+ ```python
30
+ import sqlite3
31
+
32
+ from sqlstratum import SELECT, INSERT, Table, col, Runner
33
+
34
+ users = Table(
35
+ "users",
36
+ col("id", int),
37
+ col("email", str),
38
+ col("active", int),
39
+ )
40
+
41
+ conn = sqlite3.connect(":memory:")
42
+ runner = Runner(conn)
43
+ runner.exec_ddl("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, active INTEGER)")
44
+
45
+ runner.execute(INSERT(users).VALUES(email="a@b.com", active=1))
46
+ runner.execute(INSERT(users).VALUES(email="c@d.com", active=0))
47
+
48
+ q = (
49
+ SELECT(users.c.id, users.c.email)
50
+ .FROM(users)
51
+ .WHERE(users.c.active.is_true())
52
+ .HYDRATE(dict)
53
+ )
54
+
55
+ rows = runner.fetch_all(q)
56
+ print(rows)
57
+ ```
58
+
59
+ ## Why `Table` objects?
60
+ SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
61
+ provides column metadata and a stable namespace for column access, which enables predictable SQL
62
+ generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
63
+ names in joins.
64
+
65
+ ## Project Structure
66
+ - AST: immutable query nodes in `sqlstratum/ast.py`
67
+ - Compiler: SQL + params generation in `sqlstratum/compile.py`
68
+ - Runner: SQLite execution and transactions in `sqlstratum/runner.py`
69
+ - Hydration: projection rules and targets in `sqlstratum/hydrate.py`
70
+
71
+ ## SQL Debugging
72
+ SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
73
+ intentionally gated to avoid noisy output in production. Debug output requires two conditions:
74
+ - Environment variable gate: `SQLSTRATUM_DEBUG` must be truthy (`"1"`, `"true"`, `"yes"`,
75
+ case-insensitive).
76
+ - Logger gate: the `sqlstratum` logger must be DEBUG-enabled.
77
+
78
+ Why it does not work by default: Python logging defaults to WARNING level, so even if
79
+ `SQLSTRATUM_DEBUG=1` is set, DEBUG logs will not appear unless logging is configured.
80
+
81
+ To enable debugging in a development app:
82
+
83
+ Step 1 - set the environment variable:
84
+ ```
85
+ SQLSTRATUM_DEBUG=1
86
+ ```
87
+
88
+ Step 2 - configure logging early in the app:
89
+ ```python
90
+ import logging
91
+
92
+ logging.basicConfig(level=logging.DEBUG)
93
+ # or
94
+ logging.getLogger("sqlstratum").setLevel(logging.DEBUG)
95
+ ```
96
+
97
+ Output looks like:
98
+ ```
99
+ SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
100
+ ```
101
+
102
+ Architectural intent: logging happens at the Runner boundary (after execution). AST building and
103
+ compilation remain deterministic and side-effect free, preserving separation of concerns.
104
+
105
+ ## Logo Inspiration
106
+
107
+ Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
108
+ Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/wiki/Vinicunca) for
109
+ background.
110
+
111
+ ## Versioning / Roadmap
112
+ Current version: `0.1.0`.
113
+ Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
114
+ minimal at this stage and will evolve with real usage.
115
+
116
+ ## Authorship
117
+ [Antonio Ognio](https://github.com/aognio/) is the maintainer and author of SQLStratum. ChatGPT is used for brainstorming,
118
+ architectural thinking, documentation drafting, and project management advisory. Codex (CLI/agentic
119
+ coding) is used to implement many code changes under Antonio's direction and review. The maintainer
120
+ reviews and curates changes; AI tools are assistants, not owners, and accountability remains with the
121
+ maintainer.
122
+
123
+ ## License
124
+ MIT License.
125
+
126
+ ## Contributing
127
+ PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sqlstratum"
7
+ version = "0.1.0"
8
+ description = "Lightweight, source-first SQL AST + compiler + runner."
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ license = { file = "LICENSE" }
11
+ authors = [
12
+ { name = "Antonio Ognio", email = "aognio@gmail.com" },
13
+ ]
14
+ keywords = ["sql", "query-builder", "sqlite", "compiler", "ast"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Database",
28
+ "Topic :: Software Development :: Libraries",
29
+ ]
30
+ requires-python = ">=3.8"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/aognio/sqlstratum"
34
+ Repository = "https://github.com/aognio/sqlstratum"
35
+ Issues = "https://github.com/aognio/sqlstratum/issues"
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "build>=1.2.0",
40
+ "twine>=5.0.0",
41
+ ]
42
+
43
+ [tool.setuptools]
44
+ packages = ["sqlstratum"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ """sqlstratum: minimal SQL AST + compiler + sqlite runner."""
2
+ from .dsl import SELECT, INSERT, UPDATE, DELETE, OR, AND, NOT
3
+ from .expr import COUNT, SUM, AVG, MIN, MAX
4
+ from .meta import Table, Column, col
5
+ from .compile import compile
6
+ from .runner import Runner
7
+ from .types import Expression, HydrationTarget, Hydrator, Predicate, Source
8
+
9
+ __all__ = [
10
+ "SELECT",
11
+ "INSERT",
12
+ "UPDATE",
13
+ "DELETE",
14
+ "OR",
15
+ "AND",
16
+ "NOT",
17
+ "COUNT",
18
+ "SUM",
19
+ "AVG",
20
+ "MIN",
21
+ "MAX",
22
+ "Table",
23
+ "Column",
24
+ "col",
25
+ "compile",
26
+ "Runner",
27
+ "Expression",
28
+ "HydrationTarget",
29
+ "Hydrator",
30
+ "Predicate",
31
+ "Source",
32
+ ]
@@ -0,0 +1,96 @@
1
+ """AST node definitions for sqlstratum."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, TypeVar
6
+
7
+ from .meta import Column
8
+ from .expr import OrderSpec
9
+ from .types import Expression, HydrationTarget, Predicate, Source
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Compiled:
14
+ sql: str
15
+ params: Dict[str, Any]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SelectQuery:
20
+ projections: Tuple[Expression, ...]
21
+ from_: Optional[Source]
22
+ joins: Tuple["Join", ...]
23
+ where: Tuple[Predicate, ...]
24
+ group_by: Tuple[Expression, ...]
25
+ having: Tuple[Predicate, ...]
26
+ order_by: Tuple[OrderSpec, ...]
27
+ limit: Optional[int]
28
+ offset: Optional[int]
29
+ distinct: bool
30
+ hydrate: HydrationTarget
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Join:
35
+ kind: str # "INNER" or "LEFT"
36
+ source: Source
37
+ on: Predicate
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class InsertQuery:
42
+ table: Any
43
+ values: Tuple[Tuple[str, Any], ...]
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class UpdateQuery:
48
+ table: Any
49
+ values: Tuple[Tuple[str, Any], ...]
50
+ where: Tuple[Predicate, ...]
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class DeleteQuery:
55
+ table: Any
56
+ where: Tuple[Predicate, ...]
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Subquery:
61
+ query: SelectQuery
62
+ alias: str
63
+ c: Any = None
64
+
65
+ def __post_init__(self) -> None:
66
+ object.__setattr__(self, "c", _SubqueryColumnAccessor(self.alias))
67
+
68
+ def __getattr__(self, item: str) -> Column:
69
+ return getattr(self.c, item)
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class _SubqueryTable:
74
+ name: str
75
+ alias: Optional[str] = None
76
+
77
+
78
+ class _SubqueryColumnAccessor:
79
+ def __init__(self, alias: str) -> None:
80
+ self._alias = alias
81
+
82
+ def __getattr__(self, item: str) -> Column:
83
+ return Column(name=item, py_type=object, table=_SubqueryTable(self._alias))
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class ExecutionResult:
88
+ rowcount: int
89
+ lastrowid: Optional[int]
90
+
91
+
92
+ T = TypeVar("T")
93
+
94
+
95
+ def tupled(items: Iterable[T]) -> Tuple[T, ...]:
96
+ return tuple(items)
@@ -0,0 +1,156 @@
1
+ """SQLite compiler for sqlstratum AST."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, Iterable, List
6
+
7
+ from . import ast
8
+ from .expr import (
9
+ AliasExpr,
10
+ BinaryPredicate,
11
+ Function,
12
+ Literal,
13
+ LogicalPredicate,
14
+ NotPredicate,
15
+ OrderSpec,
16
+ UnaryPredicate,
17
+ )
18
+ from .meta import Column, Table
19
+ from .types import Predicate
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Compiled(ast.Compiled):
24
+ pass
25
+
26
+
27
+ def compile(query: Any, dialect: str = "sqlite") -> Compiled:
28
+ if dialect != "sqlite":
29
+ raise ValueError("Only sqlite dialect is supported in v0.1")
30
+ compiler = _Compiler()
31
+ sql = compiler.compile_query(query)
32
+ return Compiled(sql=sql, params=compiler.params)
33
+
34
+
35
+ class _Compiler:
36
+ def __init__(self) -> None:
37
+ self.params: Dict[str, Any] = {}
38
+ self._param_index = 0
39
+
40
+ def compile_query(self, query: Any) -> str:
41
+ if isinstance(query, ast.SelectQuery):
42
+ return self._compile_select(query)
43
+ if isinstance(query, ast.InsertQuery):
44
+ return self._compile_insert(query)
45
+ if isinstance(query, ast.UpdateQuery):
46
+ return self._compile_update(query)
47
+ if isinstance(query, ast.DeleteQuery):
48
+ return self._compile_delete(query)
49
+ raise TypeError(f"Unsupported query type: {type(query)}")
50
+
51
+ def _compile_select(self, query: ast.SelectQuery) -> str:
52
+ parts: List[str] = []
53
+ distinct = "DISTINCT " if query.distinct else ""
54
+ projections = ", ".join(self._compile_projection(p) for p in query.projections)
55
+ parts.append(f"SELECT {distinct}{projections}")
56
+ if query.from_ is not None:
57
+ parts.append("FROM " + self._compile_source(query.from_))
58
+ for join in query.joins:
59
+ join_sql = "JOIN" if join.kind == "INNER" else "LEFT JOIN"
60
+ parts.append(f"{join_sql} {self._compile_source(join.source)} ON {self._compile_predicate(join.on)}")
61
+ if query.where:
62
+ parts.append("WHERE " + self._compile_and_list(query.where))
63
+ if query.group_by:
64
+ parts.append("GROUP BY " + ", ".join(self._compile_expr(e) for e in query.group_by))
65
+ if query.having:
66
+ parts.append("HAVING " + self._compile_and_list(query.having))
67
+ if query.order_by:
68
+ parts.append("ORDER BY " + ", ".join(self._compile_order(o) for o in query.order_by))
69
+ if query.limit is not None:
70
+ parts.append("LIMIT " + self._bind(query.limit))
71
+ if query.offset is not None:
72
+ parts.append("OFFSET " + self._bind(query.offset))
73
+ return " ".join(parts)
74
+
75
+ def _compile_insert(self, query: ast.InsertQuery) -> str:
76
+ columns = ", ".join(self._quote(k) for k, _ in query.values)
77
+ values = ", ".join(self._bind(v) for _, v in query.values)
78
+ return f"INSERT INTO {self._compile_table(query.table)} ({columns}) VALUES ({values})"
79
+
80
+ def _compile_update(self, query: ast.UpdateQuery) -> str:
81
+ sets = ", ".join(f"{self._quote(k)} = {self._bind(v)}" for k, v in query.values)
82
+ parts = [f"UPDATE {self._compile_table(query.table)} SET {sets}"]
83
+ if query.where:
84
+ parts.append("WHERE " + self._compile_and_list(query.where))
85
+ return " ".join(parts)
86
+
87
+ def _compile_delete(self, query: ast.DeleteQuery) -> str:
88
+ parts = [f"DELETE FROM {self._compile_table(query.table)}"]
89
+ if query.where:
90
+ parts.append("WHERE " + self._compile_and_list(query.where))
91
+ return " ".join(parts)
92
+
93
+ def _compile_projection(self, expr: Any) -> str:
94
+ return self._compile_expr(expr)
95
+
96
+ def _compile_source(self, source: Any) -> str:
97
+ if isinstance(source, Table):
98
+ return self._compile_table(source)
99
+ if isinstance(source, ast.Subquery):
100
+ return f"({self._compile_select(source.query)}) AS {self._quote(source.alias)}"
101
+ raise TypeError(f"Unsupported source type: {type(source)}")
102
+
103
+ def _compile_table(self, table: Table) -> str:
104
+ if table.alias:
105
+ return f"{self._quote(table.name)} AS {self._quote(table.alias)}"
106
+ return self._quote(table.name)
107
+
108
+ def _compile_expr(self, expr: Any) -> str:
109
+ if isinstance(expr, Column):
110
+ return self._compile_column(expr)
111
+ if isinstance(expr, AliasExpr):
112
+ return f"{self._compile_expr(expr.expr)} AS {self._quote(expr.alias)}"
113
+ if isinstance(expr, Function):
114
+ args = ", ".join(self._compile_expr(a) for a in expr.args)
115
+ return f"{expr.name}({args})"
116
+ if isinstance(expr, OrderSpec):
117
+ return self._compile_order(expr)
118
+ if isinstance(expr, Literal):
119
+ return self._bind(expr.value)
120
+ if isinstance(expr, ast.Subquery):
121
+ return f"({self._compile_select(expr.query)})"
122
+ return self._compile_predicate(expr)
123
+
124
+ def _compile_predicate(self, pred: Predicate) -> str:
125
+ if isinstance(pred, BinaryPredicate):
126
+ return f"{self._compile_expr(pred.left)} {pred.op} {self._compile_expr(pred.right)}"
127
+ if isinstance(pred, UnaryPredicate):
128
+ return f"{self._compile_expr(pred.expr)} {pred.op}"
129
+ if isinstance(pred, LogicalPredicate):
130
+ inner = f" {pred.op} ".join(self._compile_predicate(p) for p in pred.predicates)
131
+ return f"({inner})"
132
+ if isinstance(pred, NotPredicate):
133
+ return f"NOT ({self._compile_predicate(pred.predicate)})"
134
+ raise TypeError(f"Unsupported predicate type: {type(pred)}")
135
+
136
+ def _compile_and_list(self, preds: Iterable[Predicate]) -> str:
137
+ parts = [self._compile_predicate(p) for p in preds]
138
+ return " AND ".join(parts)
139
+
140
+ def _compile_order(self, order: OrderSpec) -> str:
141
+ return f"{self._compile_expr(order.expr)} {order.direction}"
142
+
143
+ def _compile_column(self, col: Column) -> str:
144
+ if col.table.alias:
145
+ return f"{self._quote(col.table.alias)}.{self._quote(col.name)}"
146
+ return f"{self._quote(col.table.name)}.{self._quote(col.name)}"
147
+
148
+ def _quote(self, ident: str) -> str:
149
+ escaped = ident.replace('"', '""')
150
+ return f'"{escaped}"'
151
+
152
+ def _bind(self, value: Any) -> str:
153
+ name = f"p{self._param_index}"
154
+ self._param_index += 1
155
+ self.params[name] = value
156
+ return f":{name}"