sqlpiston 0.1.0__py3-none-any.whl
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.
- sqlpiston/__init__.py +27 -0
- sqlpiston/_types.py +5 -0
- sqlpiston/builder/__init__.py +0 -0
- sqlpiston/builder/ddl.py +228 -0
- sqlpiston/builder/dml.py +124 -0
- sqlpiston/builder/nodes.py +248 -0
- sqlpiston/builder/selectable.py +153 -0
- sqlpiston/compiler/__init__.py +0 -0
- sqlpiston/compiler/base.py +581 -0
- sqlpiston/compiler/mysql.py +50 -0
- sqlpiston/compiler/sqlite.py +51 -0
- sqlpiston/core/__init__.py +0 -0
- sqlpiston/core/engine/__init__.py +3 -0
- sqlpiston/core/engine/base.py +99 -0
- sqlpiston/core/engine/mysql.py +80 -0
- sqlpiston/core/engine/sqlite.py +76 -0
- sqlpiston/core/pool.py +49 -0
- sqlpiston/core/session.py +61 -0
- sqlpiston/orm/__init__.py +0 -0
- sqlpiston/orm/mapper.py +68 -0
- sqlpiston-0.1.0.dist-info/METADATA +180 -0
- sqlpiston-0.1.0.dist-info/RECORD +25 -0
- sqlpiston-0.1.0.dist-info/WHEEL +5 -0
- sqlpiston-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlpiston-0.1.0.dist-info/top_level.txt +1 -0
sqlpiston/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from sqlpiston._types import ColumnValue, SQLValue
|
|
2
|
+
from sqlpiston.builder.ddl import (
|
|
3
|
+
AlterTable, ColumnDef, CreateIndex, CreateTable, CreateView,
|
|
4
|
+
DropIndex, DropTable, DropView, Truncate,
|
|
5
|
+
)
|
|
6
|
+
from sqlpiston.builder.dml import Delete, Insert, Update, Upsert
|
|
7
|
+
from sqlpiston.builder.nodes import CaseNode, Field, SQLFunction
|
|
8
|
+
from sqlpiston.builder.selectable import CTE, CompoundSelect, Select
|
|
9
|
+
from sqlpiston.compiler.base import Dialect
|
|
10
|
+
from sqlpiston.core.engine import DBType
|
|
11
|
+
from sqlpiston.core.engine.base import DBEngine
|
|
12
|
+
from sqlpiston.core.pool import ConnectionPool
|
|
13
|
+
from sqlpiston.core.session import Session
|
|
14
|
+
from sqlpiston.orm.mapper import ResultSet
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'SQLValue', 'ColumnValue',
|
|
18
|
+
'AlterTable', 'ColumnDef', 'CreateIndex', 'CreateTable', 'CreateView',
|
|
19
|
+
'DropIndex', 'DropTable', 'DropView', 'Truncate',
|
|
20
|
+
'Delete', 'Insert', 'Update', 'Upsert',
|
|
21
|
+
'CaseNode', 'Field', 'SQLFunction',
|
|
22
|
+
'CTE', 'CompoundSelect', 'Select',
|
|
23
|
+
'Dialect',
|
|
24
|
+
'DBType', 'DBEngine',
|
|
25
|
+
'ConnectionPool', 'Session',
|
|
26
|
+
'ResultSet',
|
|
27
|
+
]
|
sqlpiston/_types.py
ADDED
|
File without changes
|
sqlpiston/builder/ddl.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from sqlpiston.builder.nodes import ASTNode
|
|
6
|
+
from sqlpiston._types import SQLValue
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sqlpiston.builder.selectable import Select
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AlterAction(Enum):
|
|
13
|
+
ADD = 1
|
|
14
|
+
DROP = 2
|
|
15
|
+
MODIFY = 3
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ColumnDef:
|
|
20
|
+
"""Column definition for CREATE TABLE."""
|
|
21
|
+
name: str
|
|
22
|
+
type_: str
|
|
23
|
+
nullable: bool = True
|
|
24
|
+
primary_key: bool = False
|
|
25
|
+
default: Optional[SQLValue] = None
|
|
26
|
+
unique: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CreateTable(ASTNode):
|
|
30
|
+
"""CREATE TABLE name (col1 TYPE, col2 TYPE, ...)"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._table: Optional[str] = None
|
|
34
|
+
self._if_not_exists: bool = False
|
|
35
|
+
self._columns: List[ColumnDef] = []
|
|
36
|
+
|
|
37
|
+
def table(self, name: str) -> 'CreateTable':
|
|
38
|
+
self._table = name
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def if_not_exists(self) -> 'CreateTable':
|
|
42
|
+
self._if_not_exists = True
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def column(self, name: str, type_: str, *,
|
|
46
|
+
nullable: bool = True,
|
|
47
|
+
primary_key: bool = False,
|
|
48
|
+
default: Optional[SQLValue] = None,
|
|
49
|
+
unique: bool = False) -> 'CreateTable':
|
|
50
|
+
self._columns.append(ColumnDef(
|
|
51
|
+
name=name, type_=type_, nullable=nullable,
|
|
52
|
+
primary_key=primary_key, default=default, unique=unique,
|
|
53
|
+
))
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def columns(self, *col_defs: ColumnDef) -> 'CreateTable':
|
|
57
|
+
self._columns.extend(col_defs)
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return f"CreateTable(table={self._table!r}, columns={len(self._columns)})"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AlterTable(ASTNode):
|
|
65
|
+
"""ALTER TABLE name ADD/DROP/MODIFY COLUMN ..."""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
self._table: Optional[str] = None
|
|
69
|
+
self._actions: List[Tuple[AlterAction, str, Optional[str], Optional[ColumnDef]]] = []
|
|
70
|
+
|
|
71
|
+
def table(self, name: str) -> 'AlterTable':
|
|
72
|
+
self._table = name
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def add_column(self, name: str, type_: str, *,
|
|
76
|
+
nullable: bool = True,
|
|
77
|
+
default: Optional[SQLValue] = None) -> 'AlterTable':
|
|
78
|
+
col_def = ColumnDef(name=name, type_=type_, nullable=nullable, default=default)
|
|
79
|
+
self._actions.append((AlterAction.ADD, name, type_, col_def))
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def drop_column(self, name: str) -> 'AlterTable':
|
|
83
|
+
self._actions.append((AlterAction.DROP, name, None, None))
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def modify_column(self, name: str, type_: str, *,
|
|
87
|
+
nullable: bool = True,
|
|
88
|
+
default: Optional[SQLValue] = None) -> 'AlterTable':
|
|
89
|
+
col_def = ColumnDef(name=name, type_=type_, nullable=nullable, default=default)
|
|
90
|
+
self._actions.append((AlterAction.MODIFY, name, type_, col_def))
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return f"AlterTable(table={self._table!r}, actions={len(self._actions)})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DropTable(ASTNode):
|
|
98
|
+
"""DROP TABLE [IF EXISTS] name"""
|
|
99
|
+
|
|
100
|
+
def __init__(self) -> None:
|
|
101
|
+
self._table: Optional[str] = None
|
|
102
|
+
self._if_exists: bool = False
|
|
103
|
+
|
|
104
|
+
def table(self, name: str) -> 'DropTable':
|
|
105
|
+
self._table = name
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def if_exists(self) -> 'DropTable':
|
|
109
|
+
self._if_exists = True
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def __repr__(self) -> str:
|
|
113
|
+
return f"DropTable(table={self._table!r}, if_exists={self._if_exists})"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CreateIndex(ASTNode):
|
|
117
|
+
"""CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col1, col2, ...)"""
|
|
118
|
+
|
|
119
|
+
def __init__(self) -> None:
|
|
120
|
+
self._name: Optional[str] = None
|
|
121
|
+
self._table: Optional[str] = None
|
|
122
|
+
self._columns: Tuple[str, ...] = ()
|
|
123
|
+
self._unique: bool = False
|
|
124
|
+
self._if_not_exists: bool = False
|
|
125
|
+
|
|
126
|
+
def name(self, idx_name: str) -> 'CreateIndex':
|
|
127
|
+
self._name = idx_name
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def on(self, table: str) -> 'CreateIndex':
|
|
131
|
+
self._table = table
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def columns(self, *cols: str) -> 'CreateIndex':
|
|
135
|
+
self._columns = cols
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def unique(self) -> 'CreateIndex':
|
|
139
|
+
self._unique = True
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def if_not_exists(self) -> 'CreateIndex':
|
|
143
|
+
self._if_not_exists = True
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def __repr__(self) -> str:
|
|
147
|
+
return f"CreateIndex(name={self._name!r}, table={self._table!r})"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DropIndex(ASTNode):
|
|
151
|
+
"""DROP INDEX [IF EXISTS] name ON table"""
|
|
152
|
+
|
|
153
|
+
def __init__(self) -> None:
|
|
154
|
+
self._name: Optional[str] = None
|
|
155
|
+
self._table: Optional[str] = None
|
|
156
|
+
self._if_exists: bool = False
|
|
157
|
+
|
|
158
|
+
def name(self, idx_name: str) -> 'DropIndex':
|
|
159
|
+
self._name = idx_name
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def on(self, table: str) -> 'DropIndex':
|
|
163
|
+
self._table = table
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def if_exists(self) -> 'DropIndex':
|
|
167
|
+
self._if_exists = True
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
def __repr__(self) -> str:
|
|
171
|
+
return f"DropIndex(name={self._name!r}, table={self._table!r})"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class CreateView(ASTNode):
|
|
175
|
+
"""CREATE VIEW [IF NOT EXISTS] name AS SELECT ..."""
|
|
176
|
+
|
|
177
|
+
def __init__(self) -> None:
|
|
178
|
+
self._name: Optional[str] = None
|
|
179
|
+
self._select: Optional['Select'] = None
|
|
180
|
+
self._if_not_exists: bool = False
|
|
181
|
+
|
|
182
|
+
def name(self, view_name: str) -> 'CreateView':
|
|
183
|
+
self._name = view_name
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def as_(self, select: 'Select') -> 'CreateView':
|
|
187
|
+
self._select = select
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def if_not_exists(self) -> 'CreateView':
|
|
191
|
+
self._if_not_exists = True
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def __repr__(self) -> str:
|
|
195
|
+
return f"CreateView(name={self._name!r})"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class DropView(ASTNode):
|
|
199
|
+
"""DROP VIEW [IF EXISTS] name"""
|
|
200
|
+
|
|
201
|
+
def __init__(self) -> None:
|
|
202
|
+
self._name: Optional[str] = None
|
|
203
|
+
self._if_exists: bool = False
|
|
204
|
+
|
|
205
|
+
def name(self, view_name: str) -> 'DropView':
|
|
206
|
+
self._name = view_name
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def if_exists(self) -> 'DropView':
|
|
210
|
+
self._if_exists = True
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def __repr__(self) -> str:
|
|
214
|
+
return f"DropView(name={self._name!r})"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Truncate(ASTNode):
|
|
218
|
+
"""TRUNCATE TABLE name"""
|
|
219
|
+
|
|
220
|
+
def __init__(self) -> None:
|
|
221
|
+
self._table: Optional[str] = None
|
|
222
|
+
|
|
223
|
+
def table(self, name: str) -> 'Truncate':
|
|
224
|
+
self._table = name
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __repr__(self) -> str:
|
|
228
|
+
return f"Truncate(table={self._table!r})"
|
sqlpiston/builder/dml.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from sqlpiston.builder.nodes import ASTNode
|
|
4
|
+
from sqlpiston._types import ColumnValue
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from sqlpiston.builder.selectable import Select
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Insert(ASTNode):
|
|
11
|
+
"""INSERT INTO table (col1, col2) VALUES (v1, v2)
|
|
12
|
+
INSERT INTO table (col1, col2) SELECT ...
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._table: Optional[str] = None
|
|
17
|
+
self._data: Optional[Dict[str, ColumnValue]] = None
|
|
18
|
+
self._select: Optional['Select'] = None
|
|
19
|
+
|
|
20
|
+
def into(self, table: str) -> 'Insert':
|
|
21
|
+
self._table = table
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def values(self, data: Dict[str, ColumnValue]) -> 'Insert':
|
|
25
|
+
self._data = data
|
|
26
|
+
self._select = None
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def select(self, select: 'Select') -> 'Insert':
|
|
30
|
+
self._select = select
|
|
31
|
+
self._data = None
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return f"Insert(into={self._table!r})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Update(ASTNode):
|
|
39
|
+
"""UPDATE table SET col1=v1, col2=v2 WHERE condition"""
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self._table: Optional[str] = None
|
|
43
|
+
self._data: Optional[Dict[str, ColumnValue]] = None
|
|
44
|
+
self._where: Optional[ASTNode] = None
|
|
45
|
+
|
|
46
|
+
def table(self, name: str) -> 'Update':
|
|
47
|
+
self._table = name
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def set(self, data: Dict[str, ColumnValue]) -> 'Update':
|
|
51
|
+
self._data = data
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def where(self, condition: ASTNode) -> 'Update':
|
|
55
|
+
self._where = condition
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return f"Update(table={self._table!r})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Delete(ASTNode):
|
|
63
|
+
"""DELETE FROM table WHERE condition"""
|
|
64
|
+
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
self._table: Optional[str] = None
|
|
67
|
+
self._where: Optional[ASTNode] = None
|
|
68
|
+
|
|
69
|
+
def from_table(self, table: str) -> 'Delete':
|
|
70
|
+
self._table = table
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def where(self, condition: ASTNode) -> 'Delete':
|
|
74
|
+
self._where = condition
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"Delete(from={self._table!r})"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Upsert(ASTNode):
|
|
82
|
+
"""Standard UPSERT — AST stores intent; dialect compilers translate per DB.
|
|
83
|
+
|
|
84
|
+
MySQL compiler → INSERT INTO ... VALUES (...) ON DUPLICATE KEY UPDATE ...
|
|
85
|
+
SQLite compiler → INSERT INTO ... VALUES (...) ON CONFLICT (...) DO UPDATE SET ...
|
|
86
|
+
|
|
87
|
+
Usage:
|
|
88
|
+
Upsert()
|
|
89
|
+
.into("users")
|
|
90
|
+
.values({"id": 1, "name": "X"})
|
|
91
|
+
.on_conflict("id")
|
|
92
|
+
.do_update({"name": "X"})
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self) -> None:
|
|
96
|
+
self._table: Optional[str] = None
|
|
97
|
+
self._data: Optional[Dict[str, ColumnValue]] = None
|
|
98
|
+
self._conflict_columns: Optional[Tuple[str, ...]] = None
|
|
99
|
+
self._update_data: Optional[Dict[str, ColumnValue]] = None
|
|
100
|
+
self._do_nothing: bool = False
|
|
101
|
+
|
|
102
|
+
def into(self, table: str) -> 'Upsert':
|
|
103
|
+
self._table = table
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def values(self, data: Dict[str, ColumnValue]) -> 'Upsert':
|
|
107
|
+
self._data = data
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def on_conflict(self, *columns: str) -> 'Upsert':
|
|
111
|
+
self._conflict_columns = columns
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def do_update(self, data: Dict[str, ColumnValue]) -> 'Upsert':
|
|
115
|
+
self._update_data = data
|
|
116
|
+
self._do_nothing = False
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def do_nothing(self) -> 'Upsert':
|
|
120
|
+
self._do_nothing = True
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return f"Upsert(into={self._table!r}, conflict={self._conflict_columns!r})"
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import (
|
|
3
|
+
TYPE_CHECKING, List, Literal, Optional, Sequence, Tuple, Union, cast, overload,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from sqlpiston._types import SQLValue
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sqlpiston.builder.selectable import Select
|
|
10
|
+
from sqlpiston.compiler.base import Dialect
|
|
11
|
+
|
|
12
|
+
# ExprValue: a value that can appear in an AST expression.
|
|
13
|
+
# Can be a scalar literal, another Field reference, a Select subquery, or a function call.
|
|
14
|
+
# 'Select' is a string forward-reference to avoid circular imports with selectable.py.
|
|
15
|
+
ExprValue = Union[SQLValue, 'Field', 'Select', 'SQLFunction']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Field:
|
|
19
|
+
"""Column reference. Operator overloading returns AST nodes, not values.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
Field("age") >= 18 → ComparisonNode
|
|
23
|
+
Field("id", "users") → qualified column
|
|
24
|
+
Field("name").alias("n") → name AS n
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, name: str, table: Optional[str] = None) -> None:
|
|
28
|
+
self._name = name
|
|
29
|
+
self._table = table
|
|
30
|
+
self._alias: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
parts: List[str] = []
|
|
34
|
+
if self._table:
|
|
35
|
+
parts.append(self._table)
|
|
36
|
+
parts.append(self._name)
|
|
37
|
+
base = ".".join(parts)
|
|
38
|
+
if self._alias:
|
|
39
|
+
return f"Field({base} AS {self._alias})"
|
|
40
|
+
return f"Field({base})"
|
|
41
|
+
|
|
42
|
+
def __eq__(self, value: object) -> 'ComparisonNode': # type: ignore[override] # intentional: return AST node, not bool
|
|
43
|
+
return ComparisonNode(self, '=', cast(ExprValue, value))
|
|
44
|
+
|
|
45
|
+
def __ne__(self, value: object) -> 'ComparisonNode': # type: ignore[override] # intentional: return AST node, not bool
|
|
46
|
+
return ComparisonNode(self, '!=', cast(ExprValue, value))
|
|
47
|
+
|
|
48
|
+
def __lt__(self, value: ExprValue) -> 'ComparisonNode':
|
|
49
|
+
return ComparisonNode(self, '<', value)
|
|
50
|
+
|
|
51
|
+
def __le__(self, value: ExprValue) -> 'ComparisonNode':
|
|
52
|
+
return ComparisonNode(self, '<=', value)
|
|
53
|
+
|
|
54
|
+
def __gt__(self, value: ExprValue) -> 'ComparisonNode':
|
|
55
|
+
return ComparisonNode(self, '>', value)
|
|
56
|
+
|
|
57
|
+
def __ge__(self, value: ExprValue) -> 'ComparisonNode':
|
|
58
|
+
return ComparisonNode(self, '>=', value)
|
|
59
|
+
|
|
60
|
+
def __hash__(self) -> int:
|
|
61
|
+
return hash((self._name, self._table, self._alias))
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def is_in(self, values: Sequence[SQLValue]) -> 'InNode': ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def is_in(self, values: 'Select') -> 'InNode': ...
|
|
68
|
+
|
|
69
|
+
def is_in(self, values: Union[Sequence[SQLValue], 'Select']) -> 'InNode':
|
|
70
|
+
return InNode(self, values)
|
|
71
|
+
|
|
72
|
+
def between(self, low: ExprValue, high: ExprValue) -> 'BetweenNode':
|
|
73
|
+
return BetweenNode(self, low, high)
|
|
74
|
+
|
|
75
|
+
def is_null(self) -> 'ComparisonNode':
|
|
76
|
+
return ComparisonNode(self, 'IS NULL', None)
|
|
77
|
+
|
|
78
|
+
def is_not_null(self) -> 'ComparisonNode':
|
|
79
|
+
return ComparisonNode(self, 'IS NOT NULL', None)
|
|
80
|
+
|
|
81
|
+
def alias(self, name: str) -> 'Field':
|
|
82
|
+
f = Field(self._name, self._table)
|
|
83
|
+
f._alias = name
|
|
84
|
+
return f
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def name(self) -> str:
|
|
88
|
+
return self._name
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def table(self) -> Optional[str]:
|
|
92
|
+
return self._table
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def _alias_prop(self) -> Optional[str]:
|
|
96
|
+
return self._alias
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ASTNode(ABC):
|
|
100
|
+
"""Base for all expression/statement nodes.
|
|
101
|
+
|
|
102
|
+
compile() delegates to a dialect-specific compiler via the visitor pattern.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __and__(self, other: 'ASTNode') -> 'LogicalNode':
|
|
106
|
+
return LogicalNode('AND', [self, other])
|
|
107
|
+
|
|
108
|
+
def __or__(self, other: 'ASTNode') -> 'LogicalNode':
|
|
109
|
+
return LogicalNode('OR', [self, other])
|
|
110
|
+
|
|
111
|
+
def __invert__(self) -> 'LogicalNode':
|
|
112
|
+
return LogicalNode('NOT', [self])
|
|
113
|
+
|
|
114
|
+
def compile(self, dialect: Optional['Dialect'] = None) -> Tuple[str, Tuple[ExprValue, ...]]:
|
|
115
|
+
if dialect is None:
|
|
116
|
+
from sqlpiston.compiler.base import Compiler, GenericCompiler
|
|
117
|
+
compiler: Compiler = GenericCompiler()
|
|
118
|
+
else:
|
|
119
|
+
compiler = dialect.get_compiler()
|
|
120
|
+
return compiler.process(self)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ComparisonNode(ASTNode):
|
|
124
|
+
"""leaf: field OP value
|
|
125
|
+
|
|
126
|
+
value can be:
|
|
127
|
+
- SQL literal → field OP %s (parameterized)
|
|
128
|
+
- Field → field OP other_field (table-qualified comparison)
|
|
129
|
+
- Select → field OP (SELECT ...) (scalar subquery)
|
|
130
|
+
- SQLFunction → field OP func(...)
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, field: Field, operator: str, value: ExprValue) -> None:
|
|
134
|
+
self.field = field
|
|
135
|
+
self.operator = operator
|
|
136
|
+
self.value: ExprValue = value
|
|
137
|
+
|
|
138
|
+
def __repr__(self) -> str:
|
|
139
|
+
return f"ComparisonNode({self.field} {self.operator} {self.value!r})"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class InNode(ASTNode):
|
|
143
|
+
"""leaf: field IN (...literals...) or field IN (SELECT ...)"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, field: Field, values: Union[Sequence[SQLValue], 'Select']) -> None:
|
|
146
|
+
self.field = field
|
|
147
|
+
self.values: Union[Sequence[SQLValue], 'Select'] = values
|
|
148
|
+
|
|
149
|
+
def __repr__(self) -> str:
|
|
150
|
+
return f"InNode({self.field} IN {self.values!r})"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class BetweenNode(ASTNode):
|
|
154
|
+
"""leaf: field BETWEEN low AND high"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, field: Field, low: ExprValue, high: ExprValue) -> None:
|
|
157
|
+
self.field = field
|
|
158
|
+
self.low: ExprValue = low
|
|
159
|
+
self.high: ExprValue = high
|
|
160
|
+
|
|
161
|
+
def __repr__(self) -> str:
|
|
162
|
+
return f"BetweenNode({self.field} BETWEEN {self.low!r} AND {self.high!r})"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class LogicalNode(ASTNode):
|
|
166
|
+
"""composite: AND / OR / NOT.
|
|
167
|
+
|
|
168
|
+
Same-operator flattening: A & B & C → one LogicalNode('AND', [A,B,C]),
|
|
169
|
+
not nested.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, operator: Literal['AND', 'OR', 'NOT'], children: List[ASTNode]) -> None:
|
|
173
|
+
self.operator = operator
|
|
174
|
+
self.children = children
|
|
175
|
+
|
|
176
|
+
def __and__(self, other: ASTNode) -> 'LogicalNode':
|
|
177
|
+
if self.operator == 'AND':
|
|
178
|
+
return LogicalNode('AND', self.children + [other])
|
|
179
|
+
return LogicalNode('AND', [self, other])
|
|
180
|
+
|
|
181
|
+
def __or__(self, other: ASTNode) -> 'LogicalNode':
|
|
182
|
+
if self.operator == 'OR':
|
|
183
|
+
return LogicalNode('OR', self.children + [other])
|
|
184
|
+
return LogicalNode('OR', [self, other])
|
|
185
|
+
|
|
186
|
+
def __repr__(self) -> str:
|
|
187
|
+
return f"LogicalNode({self.operator}, children={len(self.children)})"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ExistsNode(ASTNode):
|
|
191
|
+
"""EXISTS (SELECT ...) or NOT EXISTS (SELECT ...)"""
|
|
192
|
+
|
|
193
|
+
def __init__(self, select: 'Select', negated: bool = False) -> None:
|
|
194
|
+
self.select = select
|
|
195
|
+
self.negated = negated
|
|
196
|
+
|
|
197
|
+
def __repr__(self) -> str:
|
|
198
|
+
prefix = "NOT EXISTS" if self.negated else "EXISTS"
|
|
199
|
+
return f"ExistsNode({prefix} {self.select!r})"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CaseNode(ASTNode):
|
|
203
|
+
"""CASE WHEN cond1 THEN val1 WHEN cond2 THEN val2 ... [ELSE default] END
|
|
204
|
+
|
|
205
|
+
Usage:
|
|
206
|
+
CaseNode()
|
|
207
|
+
.when(Field("score") >= 90, "A")
|
|
208
|
+
.when(Field("score") >= 80, "B")
|
|
209
|
+
.else_("C")
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(self) -> None:
|
|
213
|
+
self._whens: List[Tuple[ASTNode, ExprValue]] = []
|
|
214
|
+
self._else: Optional[ExprValue] = None
|
|
215
|
+
|
|
216
|
+
def when(self, condition: ASTNode, result: ExprValue) -> 'CaseNode':
|
|
217
|
+
self._whens.append((condition, result))
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
def else_(self, default: ExprValue) -> 'CaseNode':
|
|
221
|
+
self._else = default
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
return f"CaseNode(whens={len(self._whens)}, has_else={self._else is not None})"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class SQLFunction(ASTNode):
|
|
229
|
+
"""SQL function call: COUNT(*), SUM(col), COALESCE(a, b), etc.
|
|
230
|
+
|
|
231
|
+
Usage:
|
|
232
|
+
SQLFunction("count", "*")
|
|
233
|
+
SQLFunction("coalesce", Field("name"), "N/A")
|
|
234
|
+
SQLFunction("sum", Field("amount"))
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, name: str, *args: Union[str, Field, 'SQLFunction']) -> None:
|
|
238
|
+
self.name = name
|
|
239
|
+
self.args: Tuple[Union[str, Field, 'SQLFunction'], ...] = args
|
|
240
|
+
self._alias: Optional[str] = None
|
|
241
|
+
|
|
242
|
+
def alias(self, name: str) -> 'SQLFunction':
|
|
243
|
+
self._alias = name
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def __repr__(self) -> str:
|
|
247
|
+
alias_str = f" AS {self._alias}" if self._alias else ""
|
|
248
|
+
return f"SQLFunction({self.name}({', '.join(repr(a) for a in self.args)}){alias_str})"
|