minidb-sql 1.3.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.
- minidb_sql/__init__.py +168 -0
- minidb_sql/column.py +170 -0
- minidb_sql/database.py +258 -0
- minidb_sql/errors.py +176 -0
- minidb_sql/index.py +222 -0
- minidb_sql/parser.py +1278 -0
- minidb_sql/persistence.py +72 -0
- minidb_sql/planner.py +167 -0
- minidb_sql/query.py +1077 -0
- minidb_sql/table.py +274 -0
- minidb_sql/types.py +189 -0
- minidb_sql-1.3.0.dist-info/METADATA +379 -0
- minidb_sql-1.3.0.dist-info/RECORD +16 -0
- minidb_sql-1.3.0.dist-info/WHEEL +5 -0
- minidb_sql-1.3.0.dist-info/licenses/LICENSE +21 -0
- minidb_sql-1.3.0.dist-info/top_level.txt +1 -0
minidb_sql/parser.py
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
"""SQL parser for MiniDB."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import InvalidQueryError, SyntaxError_
|
|
8
|
+
from .types import ColumnType, TokenType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Token:
|
|
13
|
+
"""Represents a single token from the lexer."""
|
|
14
|
+
|
|
15
|
+
type: TokenType
|
|
16
|
+
value: Any
|
|
17
|
+
position: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ParamPlaceholder:
|
|
22
|
+
"""Sentinel for a ? parameter that is bound after parse."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Condition:
|
|
27
|
+
"""Represents a WHERE condition."""
|
|
28
|
+
|
|
29
|
+
column: str
|
|
30
|
+
operator: str # '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'IN', 'IS NULL', 'IS NOT NULL'
|
|
31
|
+
value: Any
|
|
32
|
+
table_alias: str | None = None # For JOIN column references
|
|
33
|
+
negated: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class WhereClause:
|
|
38
|
+
"""Represents a WHERE clause with AND/OR logic."""
|
|
39
|
+
|
|
40
|
+
conditions: list # List of Condition or WhereClause (nested)
|
|
41
|
+
operator: str = 'AND' # 'AND' or 'OR'
|
|
42
|
+
|
|
43
|
+
def and_conjuncts(self) -> list[Condition]:
|
|
44
|
+
"""AND predicates, including a comparison wrapped in parentheses.
|
|
45
|
+
|
|
46
|
+
A parenthesized comparison is its own WhereClause. Nested OR is a
|
|
47
|
+
different predicate and must not become an index probe.
|
|
48
|
+
"""
|
|
49
|
+
if self.operator != 'AND':
|
|
50
|
+
return []
|
|
51
|
+
found: list[Condition] = []
|
|
52
|
+
for cond in self.conditions:
|
|
53
|
+
if isinstance(cond, Condition):
|
|
54
|
+
found.append(cond)
|
|
55
|
+
elif isinstance(cond, WhereClause) and cond.operator == 'AND':
|
|
56
|
+
found.extend(cond.and_conjuncts())
|
|
57
|
+
return found
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class JoinClause:
|
|
62
|
+
"""Represents a JOIN clause."""
|
|
63
|
+
|
|
64
|
+
table: str
|
|
65
|
+
left_column: str
|
|
66
|
+
right_column: str
|
|
67
|
+
join_type: str = 'INNER' # 'INNER', 'LEFT', 'RIGHT'
|
|
68
|
+
left_table: str | None = None
|
|
69
|
+
right_table: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class OrderByItem:
|
|
74
|
+
"""Represents an ORDER BY item."""
|
|
75
|
+
|
|
76
|
+
column: str
|
|
77
|
+
direction: str = 'ASC'
|
|
78
|
+
table_alias: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class CaseResult:
|
|
83
|
+
"""A THEN or ELSE value. column is set for a column reference."""
|
|
84
|
+
|
|
85
|
+
literal: Any = None
|
|
86
|
+
column: str | None = None
|
|
87
|
+
table_alias: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class CaseWhen:
|
|
92
|
+
"""One searched WHEN arm. condition uses the WHERE grammar."""
|
|
93
|
+
|
|
94
|
+
condition: WhereClause
|
|
95
|
+
result: CaseResult
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class CaseExpr:
|
|
100
|
+
"""Searched CASE. else_result None means no ELSE, which is NULL."""
|
|
101
|
+
|
|
102
|
+
whens: list[CaseWhen]
|
|
103
|
+
else_result: CaseResult | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class AggregateFunction:
|
|
108
|
+
"""Represents an aggregate function call."""
|
|
109
|
+
|
|
110
|
+
function: str # 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX'
|
|
111
|
+
column: str # Column name or '*' for COUNT(*)
|
|
112
|
+
alias: str | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class SelectColumn:
|
|
117
|
+
"""Represents a column in a SELECT statement."""
|
|
118
|
+
|
|
119
|
+
name: str # Column name or '*'
|
|
120
|
+
table_alias: str | None = None
|
|
121
|
+
aggregate: AggregateFunction | None = None
|
|
122
|
+
alias: str | None = None
|
|
123
|
+
case: CaseExpr | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class SelectQuery:
|
|
128
|
+
"""Represents a parsed SELECT query."""
|
|
129
|
+
|
|
130
|
+
columns: list[SelectColumn]
|
|
131
|
+
table: str
|
|
132
|
+
where: WhereClause | None = None
|
|
133
|
+
order_by: list[OrderByItem] = field(default_factory=list)
|
|
134
|
+
group_by: list[str] = field(default_factory=list)
|
|
135
|
+
having: WhereClause | None = None
|
|
136
|
+
limit: int | None = None
|
|
137
|
+
joins: list[JoinClause] = field(default_factory=list)
|
|
138
|
+
distinct: bool = False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class InsertQuery:
|
|
143
|
+
"""Represents a parsed INSERT query."""
|
|
144
|
+
|
|
145
|
+
table: str
|
|
146
|
+
columns: list[str]
|
|
147
|
+
values: list[Any]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class UpdateQuery:
|
|
152
|
+
"""Represents a parsed UPDATE query."""
|
|
153
|
+
|
|
154
|
+
table: str
|
|
155
|
+
set_clause: dict[str, Any]
|
|
156
|
+
where: WhereClause | None = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class DeleteQuery:
|
|
161
|
+
"""Represents a parsed DELETE query."""
|
|
162
|
+
|
|
163
|
+
table: str
|
|
164
|
+
where: WhereClause | None = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class ColumnDef:
|
|
169
|
+
"""Represents a column definition in CREATE TABLE."""
|
|
170
|
+
|
|
171
|
+
name: str
|
|
172
|
+
type: ColumnType
|
|
173
|
+
primary_key: bool = False
|
|
174
|
+
nullable: bool = True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class CreateTableQuery:
|
|
179
|
+
"""Represents a parsed CREATE TABLE query."""
|
|
180
|
+
|
|
181
|
+
table: str
|
|
182
|
+
columns: list[ColumnDef]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class DropTableQuery:
|
|
187
|
+
"""Represents a parsed DROP TABLE query."""
|
|
188
|
+
|
|
189
|
+
table: str
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class BeginQuery:
|
|
194
|
+
"""Represents a parsed BEGIN statement."""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class CommitQuery:
|
|
199
|
+
"""Represents a parsed COMMIT statement."""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class RollbackQuery:
|
|
204
|
+
"""Represents a parsed ROLLBACK statement."""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class Lexer:
|
|
208
|
+
"""Tokenizes SQL queries."""
|
|
209
|
+
|
|
210
|
+
KEYWORDS = {
|
|
211
|
+
'SELECT': TokenType.SELECT,
|
|
212
|
+
'FROM': TokenType.FROM,
|
|
213
|
+
'WHERE': TokenType.WHERE,
|
|
214
|
+
'INSERT': TokenType.INSERT,
|
|
215
|
+
'INTO': TokenType.INTO,
|
|
216
|
+
'VALUES': TokenType.VALUES,
|
|
217
|
+
'UPDATE': TokenType.UPDATE,
|
|
218
|
+
'SET': TokenType.SET,
|
|
219
|
+
'DELETE': TokenType.DELETE,
|
|
220
|
+
'CREATE': TokenType.CREATE,
|
|
221
|
+
'TABLE': TokenType.TABLE,
|
|
222
|
+
'DROP': TokenType.DROP,
|
|
223
|
+
'ORDER': TokenType.ORDER,
|
|
224
|
+
'BY': TokenType.BY,
|
|
225
|
+
'GROUP': TokenType.GROUP,
|
|
226
|
+
'JOIN': TokenType.JOIN,
|
|
227
|
+
'INNER': TokenType.INNER,
|
|
228
|
+
'LEFT': TokenType.LEFT,
|
|
229
|
+
'RIGHT': TokenType.RIGHT,
|
|
230
|
+
'ON': TokenType.ON,
|
|
231
|
+
'AND': TokenType.AND,
|
|
232
|
+
'OR': TokenType.OR,
|
|
233
|
+
'NOT': TokenType.NOT,
|
|
234
|
+
'IN': TokenType.IN,
|
|
235
|
+
'LIKE': TokenType.LIKE,
|
|
236
|
+
'ASC': TokenType.ASC,
|
|
237
|
+
'DESC': TokenType.DESC,
|
|
238
|
+
'PRIMARY': TokenType.PRIMARY,
|
|
239
|
+
'KEY': TokenType.KEY,
|
|
240
|
+
'NULL': TokenType.NULL,
|
|
241
|
+
'IS': TokenType.IS,
|
|
242
|
+
'LIMIT': TokenType.LIMIT,
|
|
243
|
+
'AS': TokenType.AS,
|
|
244
|
+
'DISTINCT': TokenType.DISTINCT,
|
|
245
|
+
'HAVING': TokenType.HAVING,
|
|
246
|
+
'CASE': TokenType.CASE,
|
|
247
|
+
'WHEN': TokenType.WHEN,
|
|
248
|
+
'THEN': TokenType.THEN,
|
|
249
|
+
'ELSE': TokenType.ELSE,
|
|
250
|
+
'END': TokenType.END,
|
|
251
|
+
'BEGIN': TokenType.BEGIN,
|
|
252
|
+
'COMMIT': TokenType.COMMIT,
|
|
253
|
+
'ROLLBACK': TokenType.ROLLBACK,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def __init__(self, sql: str):
|
|
257
|
+
self.sql = sql
|
|
258
|
+
self.pos = 0
|
|
259
|
+
self.tokens: list[Token] = []
|
|
260
|
+
|
|
261
|
+
def tokenize(self) -> list[Token]:
|
|
262
|
+
"""Tokenize the SQL string."""
|
|
263
|
+
while self.pos < len(self.sql):
|
|
264
|
+
self._skip_whitespace()
|
|
265
|
+
if self.pos >= len(self.sql):
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
char = self.sql[self.pos]
|
|
269
|
+
|
|
270
|
+
# String literal
|
|
271
|
+
if char in ('"', "'"):
|
|
272
|
+
self._read_string(char)
|
|
273
|
+
# Number
|
|
274
|
+
elif char.isdigit() or (char == '-' and self.pos + 1 < len(self.sql) and self.sql[self.pos + 1].isdigit()):
|
|
275
|
+
self._read_number()
|
|
276
|
+
# Identifier or keyword
|
|
277
|
+
elif char.isalpha() or char == '_':
|
|
278
|
+
self._read_identifier()
|
|
279
|
+
# Operators and punctuation
|
|
280
|
+
elif char == '=':
|
|
281
|
+
self.tokens.append(Token(TokenType.EQUALS, '=', self.pos))
|
|
282
|
+
self.pos += 1
|
|
283
|
+
elif char == '!':
|
|
284
|
+
if self.pos + 1 < len(self.sql) and self.sql[self.pos + 1] == '=':
|
|
285
|
+
self.tokens.append(Token(TokenType.NOT_EQUALS, '!=', self.pos))
|
|
286
|
+
self.pos += 2
|
|
287
|
+
else:
|
|
288
|
+
raise SyntaxError_("Unexpected character '!'", self.pos)
|
|
289
|
+
elif char == '>':
|
|
290
|
+
if self.pos + 1 < len(self.sql) and self.sql[self.pos + 1] == '=':
|
|
291
|
+
self.tokens.append(Token(TokenType.GREATER_EQUAL, '>=', self.pos))
|
|
292
|
+
self.pos += 2
|
|
293
|
+
else:
|
|
294
|
+
self.tokens.append(Token(TokenType.GREATER, '>', self.pos))
|
|
295
|
+
self.pos += 1
|
|
296
|
+
elif char == '<':
|
|
297
|
+
if self.pos + 1 < len(self.sql) and self.sql[self.pos + 1] == '=':
|
|
298
|
+
self.tokens.append(Token(TokenType.LESS_EQUAL, '<=', self.pos))
|
|
299
|
+
self.pos += 2
|
|
300
|
+
else:
|
|
301
|
+
self.tokens.append(Token(TokenType.LESS, '<', self.pos))
|
|
302
|
+
self.pos += 1
|
|
303
|
+
elif char == ',':
|
|
304
|
+
self.tokens.append(Token(TokenType.COMMA, ',', self.pos))
|
|
305
|
+
self.pos += 1
|
|
306
|
+
elif char == '.':
|
|
307
|
+
self.tokens.append(Token(TokenType.DOT, '.', self.pos))
|
|
308
|
+
self.pos += 1
|
|
309
|
+
elif char == '(':
|
|
310
|
+
self.tokens.append(Token(TokenType.LPAREN, '(', self.pos))
|
|
311
|
+
self.pos += 1
|
|
312
|
+
elif char == ')':
|
|
313
|
+
self.tokens.append(Token(TokenType.RPAREN, ')', self.pos))
|
|
314
|
+
self.pos += 1
|
|
315
|
+
elif char == ';':
|
|
316
|
+
self.tokens.append(Token(TokenType.SEMICOLON, ';', self.pos))
|
|
317
|
+
self.pos += 1
|
|
318
|
+
elif char == '*':
|
|
319
|
+
self.tokens.append(Token(TokenType.STAR, '*', self.pos))
|
|
320
|
+
self.pos += 1
|
|
321
|
+
elif char == '?':
|
|
322
|
+
self.tokens.append(Token(TokenType.PLACEHOLDER, '?', self.pos))
|
|
323
|
+
self.pos += 1
|
|
324
|
+
elif char == '-' and self.pos + 1 < len(self.sql) and self.sql[self.pos + 1] == '-':
|
|
325
|
+
self._skip_line_comment()
|
|
326
|
+
elif char == '/' and self.pos + 1 < len(self.sql) and self.sql[self.pos + 1] == '*':
|
|
327
|
+
self._skip_block_comment()
|
|
328
|
+
else:
|
|
329
|
+
raise SyntaxError_(f"Unexpected character: '{char}'", self.pos)
|
|
330
|
+
|
|
331
|
+
self.tokens.append(Token(TokenType.EOF, None, self.pos))
|
|
332
|
+
return self.tokens
|
|
333
|
+
|
|
334
|
+
def _skip_line_comment(self) -> None:
|
|
335
|
+
"""Skip a -- comment through the end of the line."""
|
|
336
|
+
self.pos += 2
|
|
337
|
+
while self.pos < len(self.sql) and self.sql[self.pos] not in '\n\r':
|
|
338
|
+
self.pos += 1
|
|
339
|
+
|
|
340
|
+
def _skip_block_comment(self) -> None:
|
|
341
|
+
"""Skip a /* */ comment. An unclosed comment is a syntax error."""
|
|
342
|
+
start = self.pos
|
|
343
|
+
self.pos += 2
|
|
344
|
+
while self.pos + 1 < len(self.sql):
|
|
345
|
+
if self.sql[self.pos] == '*' and self.sql[self.pos + 1] == '/':
|
|
346
|
+
self.pos += 2
|
|
347
|
+
return
|
|
348
|
+
self.pos += 1
|
|
349
|
+
raise SyntaxError_('Unterminated block comment', start)
|
|
350
|
+
|
|
351
|
+
def _skip_whitespace(self):
|
|
352
|
+
"""Skip whitespace characters."""
|
|
353
|
+
while self.pos < len(self.sql) and self.sql[self.pos].isspace():
|
|
354
|
+
self.pos += 1
|
|
355
|
+
|
|
356
|
+
def _read_string(self, quote_char: str):
|
|
357
|
+
"""Read a string literal."""
|
|
358
|
+
start = self.pos
|
|
359
|
+
self.pos += 1 # Skip opening quote
|
|
360
|
+
value = []
|
|
361
|
+
|
|
362
|
+
while self.pos < len(self.sql):
|
|
363
|
+
char = self.sql[self.pos]
|
|
364
|
+
if char == quote_char:
|
|
365
|
+
# Check for escaped quote
|
|
366
|
+
if self.pos + 1 < len(self.sql) and self.sql[self.pos + 1] == quote_char:
|
|
367
|
+
value.append(quote_char)
|
|
368
|
+
self.pos += 2
|
|
369
|
+
else:
|
|
370
|
+
self.pos += 1 # Skip closing quote
|
|
371
|
+
break
|
|
372
|
+
elif char == '\\' and self.pos + 1 < len(self.sql):
|
|
373
|
+
# Handle escape sequences
|
|
374
|
+
next_char = self.sql[self.pos + 1]
|
|
375
|
+
if next_char == 'n':
|
|
376
|
+
value.append('\n')
|
|
377
|
+
elif next_char == 't':
|
|
378
|
+
value.append('\t')
|
|
379
|
+
elif next_char == '\\':
|
|
380
|
+
value.append('\\')
|
|
381
|
+
elif next_char == quote_char:
|
|
382
|
+
value.append(quote_char)
|
|
383
|
+
else:
|
|
384
|
+
value.append(next_char)
|
|
385
|
+
self.pos += 2
|
|
386
|
+
else:
|
|
387
|
+
value.append(char)
|
|
388
|
+
self.pos += 1
|
|
389
|
+
else:
|
|
390
|
+
raise SyntaxError_('Unterminated string literal', start)
|
|
391
|
+
|
|
392
|
+
self.tokens.append(Token(TokenType.STRING_LITERAL, ''.join(value), start))
|
|
393
|
+
|
|
394
|
+
def _read_number(self):
|
|
395
|
+
"""Read a numeric literal."""
|
|
396
|
+
start = self.pos
|
|
397
|
+
value = []
|
|
398
|
+
|
|
399
|
+
if self.sql[self.pos] == '-':
|
|
400
|
+
value.append('-')
|
|
401
|
+
self.pos += 1
|
|
402
|
+
|
|
403
|
+
while self.pos < len(self.sql) and self.sql[self.pos].isdigit():
|
|
404
|
+
value.append(self.sql[self.pos])
|
|
405
|
+
self.pos += 1
|
|
406
|
+
|
|
407
|
+
# Check for decimal
|
|
408
|
+
if self.pos < len(self.sql) and self.sql[self.pos] == '.':
|
|
409
|
+
value.append('.')
|
|
410
|
+
self.pos += 1
|
|
411
|
+
while self.pos < len(self.sql) and self.sql[self.pos].isdigit():
|
|
412
|
+
value.append(self.sql[self.pos])
|
|
413
|
+
self.pos += 1
|
|
414
|
+
self.tokens.append(Token(TokenType.FLOAT_LITERAL, float(''.join(value)), start))
|
|
415
|
+
else:
|
|
416
|
+
self.tokens.append(Token(TokenType.INTEGER_LITERAL, int(''.join(value)), start))
|
|
417
|
+
|
|
418
|
+
def _read_identifier(self):
|
|
419
|
+
"""Read an identifier or keyword."""
|
|
420
|
+
start = self.pos
|
|
421
|
+
value = []
|
|
422
|
+
|
|
423
|
+
while self.pos < len(self.sql) and (self.sql[self.pos].isalnum() or self.sql[self.pos] == '_'):
|
|
424
|
+
value.append(self.sql[self.pos])
|
|
425
|
+
self.pos += 1
|
|
426
|
+
|
|
427
|
+
identifier = ''.join(value)
|
|
428
|
+
upper = identifier.upper()
|
|
429
|
+
|
|
430
|
+
# Check for keywords
|
|
431
|
+
if upper in self.KEYWORDS:
|
|
432
|
+
self.tokens.append(Token(self.KEYWORDS[upper], upper, start))
|
|
433
|
+
# Check for boolean literals
|
|
434
|
+
elif upper == 'TRUE':
|
|
435
|
+
self.tokens.append(Token(TokenType.BOOLEAN_LITERAL, True, start))
|
|
436
|
+
elif upper == 'FALSE':
|
|
437
|
+
self.tokens.append(Token(TokenType.BOOLEAN_LITERAL, False, start))
|
|
438
|
+
else:
|
|
439
|
+
self.tokens.append(Token(TokenType.IDENTIFIER, identifier, start))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class Parser:
|
|
443
|
+
"""Parses tokenized SQL queries."""
|
|
444
|
+
|
|
445
|
+
def __init__(self, tokens: list[Token]):
|
|
446
|
+
self.tokens = tokens
|
|
447
|
+
self.pos = 0
|
|
448
|
+
self._paren_depth = 0
|
|
449
|
+
|
|
450
|
+
def parse(
|
|
451
|
+
self,
|
|
452
|
+
) -> (
|
|
453
|
+
SelectQuery
|
|
454
|
+
| InsertQuery
|
|
455
|
+
| UpdateQuery
|
|
456
|
+
| DeleteQuery
|
|
457
|
+
| CreateTableQuery
|
|
458
|
+
| DropTableQuery
|
|
459
|
+
| BeginQuery
|
|
460
|
+
| CommitQuery
|
|
461
|
+
| RollbackQuery
|
|
462
|
+
):
|
|
463
|
+
"""Parse the token stream into a query object."""
|
|
464
|
+
token = self._current()
|
|
465
|
+
|
|
466
|
+
if token.type == TokenType.SELECT:
|
|
467
|
+
return self._parse_select()
|
|
468
|
+
elif token.type == TokenType.INSERT:
|
|
469
|
+
return self._parse_insert()
|
|
470
|
+
elif token.type == TokenType.UPDATE:
|
|
471
|
+
return self._parse_update()
|
|
472
|
+
elif token.type == TokenType.DELETE:
|
|
473
|
+
return self._parse_delete()
|
|
474
|
+
elif token.type == TokenType.CREATE:
|
|
475
|
+
return self._parse_create()
|
|
476
|
+
elif token.type == TokenType.DROP:
|
|
477
|
+
return self._parse_drop()
|
|
478
|
+
elif token.type == TokenType.BEGIN:
|
|
479
|
+
return self._parse_begin()
|
|
480
|
+
elif token.type == TokenType.COMMIT:
|
|
481
|
+
return self._parse_commit()
|
|
482
|
+
elif token.type == TokenType.ROLLBACK:
|
|
483
|
+
return self._parse_rollback()
|
|
484
|
+
else:
|
|
485
|
+
raise SyntaxError_(f'Unexpected token: {token.value}', token.position)
|
|
486
|
+
|
|
487
|
+
def _current(self) -> Token:
|
|
488
|
+
"""Get the current token."""
|
|
489
|
+
return self.tokens[self.pos] if self.pos < len(self.tokens) else self.tokens[-1]
|
|
490
|
+
|
|
491
|
+
def _peek(self, offset: int = 1) -> Token:
|
|
492
|
+
"""Peek at a future token."""
|
|
493
|
+
pos = self.pos + offset
|
|
494
|
+
return self.tokens[pos] if pos < len(self.tokens) else self.tokens[-1]
|
|
495
|
+
|
|
496
|
+
def _advance(self) -> Token:
|
|
497
|
+
"""Advance to the next token and return the previous one."""
|
|
498
|
+
token = self._current()
|
|
499
|
+
self.pos += 1
|
|
500
|
+
return token
|
|
501
|
+
|
|
502
|
+
def _expect(self, token_type: TokenType) -> Token:
|
|
503
|
+
"""Expect a specific token type."""
|
|
504
|
+
token = self._current()
|
|
505
|
+
if token.type != token_type:
|
|
506
|
+
raise SyntaxError_(f'Expected {token_type.name}, got {token.type.name}', token.position)
|
|
507
|
+
return self._advance()
|
|
508
|
+
|
|
509
|
+
def _match(self, *token_types: TokenType) -> bool:
|
|
510
|
+
"""Check if current token matches any of the given types."""
|
|
511
|
+
return self._current().type in token_types
|
|
512
|
+
|
|
513
|
+
def _expect_end(self) -> None:
|
|
514
|
+
"""Consume an optional semicolon, then require end of input."""
|
|
515
|
+
if self._match(TokenType.SEMICOLON):
|
|
516
|
+
self._advance()
|
|
517
|
+
token = self._current()
|
|
518
|
+
if token.type != TokenType.EOF:
|
|
519
|
+
raise SyntaxError_(f'Unexpected token: {token.value}', token.position)
|
|
520
|
+
|
|
521
|
+
def _parse_select(self) -> SelectQuery:
|
|
522
|
+
"""Parse a SELECT query."""
|
|
523
|
+
self._expect(TokenType.SELECT)
|
|
524
|
+
|
|
525
|
+
distinct = False
|
|
526
|
+
if self._match(TokenType.DISTINCT):
|
|
527
|
+
self._advance()
|
|
528
|
+
distinct = True
|
|
529
|
+
|
|
530
|
+
# Parse columns
|
|
531
|
+
columns = self._parse_select_columns()
|
|
532
|
+
|
|
533
|
+
# FROM clause
|
|
534
|
+
self._expect(TokenType.FROM)
|
|
535
|
+
table_token = self._expect(TokenType.IDENTIFIER)
|
|
536
|
+
table = table_token.value
|
|
537
|
+
|
|
538
|
+
# JOIN clauses
|
|
539
|
+
joins = []
|
|
540
|
+
while self._match(TokenType.JOIN, TokenType.INNER, TokenType.LEFT, TokenType.RIGHT):
|
|
541
|
+
joins.append(self._parse_join())
|
|
542
|
+
|
|
543
|
+
# WHERE clause
|
|
544
|
+
where = None
|
|
545
|
+
if self._match(TokenType.WHERE):
|
|
546
|
+
self._advance()
|
|
547
|
+
where = self._parse_where()
|
|
548
|
+
|
|
549
|
+
# GROUP BY clause
|
|
550
|
+
group_by = []
|
|
551
|
+
if self._match(TokenType.GROUP):
|
|
552
|
+
self._advance()
|
|
553
|
+
self._expect(TokenType.BY)
|
|
554
|
+
group_by = self._parse_group_by()
|
|
555
|
+
|
|
556
|
+
# HAVING clause
|
|
557
|
+
having = None
|
|
558
|
+
if self._match(TokenType.HAVING):
|
|
559
|
+
self._advance()
|
|
560
|
+
having = self._parse_where()
|
|
561
|
+
|
|
562
|
+
# ORDER BY clause
|
|
563
|
+
order_by = []
|
|
564
|
+
if self._match(TokenType.ORDER):
|
|
565
|
+
self._advance()
|
|
566
|
+
self._expect(TokenType.BY)
|
|
567
|
+
order_by = self._parse_order_by()
|
|
568
|
+
|
|
569
|
+
# LIMIT clause
|
|
570
|
+
limit = None
|
|
571
|
+
if self._match(TokenType.LIMIT):
|
|
572
|
+
self._advance()
|
|
573
|
+
limit_token = self._expect(TokenType.INTEGER_LITERAL)
|
|
574
|
+
limit = limit_token.value
|
|
575
|
+
|
|
576
|
+
self._expect_end()
|
|
577
|
+
|
|
578
|
+
return SelectQuery(
|
|
579
|
+
columns=columns,
|
|
580
|
+
table=table,
|
|
581
|
+
where=where,
|
|
582
|
+
order_by=order_by,
|
|
583
|
+
group_by=group_by,
|
|
584
|
+
having=having,
|
|
585
|
+
limit=limit,
|
|
586
|
+
joins=joins,
|
|
587
|
+
distinct=distinct,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
def _parse_select_columns(self) -> list[SelectColumn]:
|
|
591
|
+
"""Parse SELECT column list."""
|
|
592
|
+
columns = []
|
|
593
|
+
|
|
594
|
+
while True:
|
|
595
|
+
col = self._parse_select_column()
|
|
596
|
+
columns.append(col)
|
|
597
|
+
|
|
598
|
+
if not self._match(TokenType.COMMA):
|
|
599
|
+
break
|
|
600
|
+
self._advance()
|
|
601
|
+
|
|
602
|
+
return columns
|
|
603
|
+
|
|
604
|
+
def _parse_select_column(self) -> SelectColumn:
|
|
605
|
+
"""Parse a single SELECT column."""
|
|
606
|
+
if self._match(TokenType.CASE):
|
|
607
|
+
return self._parse_case_column()
|
|
608
|
+
|
|
609
|
+
# Check for aggregate functions
|
|
610
|
+
if self._match(TokenType.IDENTIFIER):
|
|
611
|
+
name = self._current().value.upper()
|
|
612
|
+
if name in ('COUNT', 'SUM', 'AVG', 'MIN', 'MAX'):
|
|
613
|
+
return self._parse_aggregate_column()
|
|
614
|
+
|
|
615
|
+
# Check for *
|
|
616
|
+
if self._match(TokenType.STAR):
|
|
617
|
+
self._advance()
|
|
618
|
+
return SelectColumn(name='*')
|
|
619
|
+
|
|
620
|
+
# Check for table.column or just column
|
|
621
|
+
table_alias = None
|
|
622
|
+
col_name = self._expect(TokenType.IDENTIFIER).value
|
|
623
|
+
|
|
624
|
+
if self._match(TokenType.DOT):
|
|
625
|
+
self._advance()
|
|
626
|
+
if self._match(TokenType.STAR):
|
|
627
|
+
self._advance()
|
|
628
|
+
return SelectColumn(name='*', table_alias=col_name)
|
|
629
|
+
table_alias = col_name
|
|
630
|
+
col_name = self._expect(TokenType.IDENTIFIER).value
|
|
631
|
+
|
|
632
|
+
alias = None
|
|
633
|
+
if self._match(TokenType.AS):
|
|
634
|
+
self._advance()
|
|
635
|
+
alias = self._expect(TokenType.IDENTIFIER).value
|
|
636
|
+
elif self._match(TokenType.IDENTIFIER):
|
|
637
|
+
alias = self._advance().value
|
|
638
|
+
|
|
639
|
+
return SelectColumn(name=col_name, table_alias=table_alias, alias=alias)
|
|
640
|
+
|
|
641
|
+
def _parse_aggregate_column(self) -> SelectColumn:
|
|
642
|
+
"""Parse an aggregate function column."""
|
|
643
|
+
func_name = self._advance().value.upper()
|
|
644
|
+
self._expect(TokenType.LPAREN)
|
|
645
|
+
|
|
646
|
+
table_alias = None
|
|
647
|
+
if self._match(TokenType.STAR):
|
|
648
|
+
self._advance()
|
|
649
|
+
col_name = '*'
|
|
650
|
+
else:
|
|
651
|
+
col_name = self._expect(TokenType.IDENTIFIER).value
|
|
652
|
+
# Check for table.column syntax
|
|
653
|
+
if self._match(TokenType.DOT):
|
|
654
|
+
self._advance()
|
|
655
|
+
table_alias = col_name
|
|
656
|
+
col_name = self._expect(TokenType.IDENTIFIER).value
|
|
657
|
+
|
|
658
|
+
self._expect(TokenType.RPAREN)
|
|
659
|
+
|
|
660
|
+
alias = None
|
|
661
|
+
if self._match(TokenType.AS):
|
|
662
|
+
self._advance()
|
|
663
|
+
alias = self._expect(TokenType.IDENTIFIER).value
|
|
664
|
+
elif self._match(TokenType.IDENTIFIER):
|
|
665
|
+
alias = self._advance().value
|
|
666
|
+
|
|
667
|
+
return SelectColumn(
|
|
668
|
+
name=col_name,
|
|
669
|
+
table_alias=table_alias,
|
|
670
|
+
aggregate=AggregateFunction(function=func_name, column=col_name, alias=alias),
|
|
671
|
+
alias=alias,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
def _parse_case_column(self) -> SelectColumn:
|
|
675
|
+
"""Parse CASE WHEN ... THEN ... [ELSE ...] END [AS alias]."""
|
|
676
|
+
self._expect(TokenType.CASE)
|
|
677
|
+
if not self._match(TokenType.WHEN):
|
|
678
|
+
token = self._current()
|
|
679
|
+
raise SyntaxError_(f'Expected WHEN, got {token.type.name}', token.position)
|
|
680
|
+
|
|
681
|
+
whens: list[CaseWhen] = []
|
|
682
|
+
while self._match(TokenType.WHEN):
|
|
683
|
+
self._advance()
|
|
684
|
+
condition = self._parse_or()
|
|
685
|
+
self._expect(TokenType.THEN)
|
|
686
|
+
whens.append(CaseWhen(condition=condition, result=self._parse_case_result()))
|
|
687
|
+
|
|
688
|
+
else_result = None
|
|
689
|
+
if self._match(TokenType.ELSE):
|
|
690
|
+
self._advance()
|
|
691
|
+
else_result = self._parse_case_result()
|
|
692
|
+
self._expect(TokenType.END)
|
|
693
|
+
|
|
694
|
+
alias = None
|
|
695
|
+
if self._match(TokenType.AS):
|
|
696
|
+
self._advance()
|
|
697
|
+
alias = self._expect(TokenType.IDENTIFIER).value
|
|
698
|
+
elif self._match(TokenType.IDENTIFIER):
|
|
699
|
+
alias = self._advance().value
|
|
700
|
+
|
|
701
|
+
return SelectColumn(name='CASE', alias=alias, case=CaseExpr(whens=whens, else_result=else_result))
|
|
702
|
+
|
|
703
|
+
def _parse_case_result(self) -> CaseResult:
|
|
704
|
+
"""Parse a THEN or ELSE result: literal, parameter, or column."""
|
|
705
|
+
token = self._current()
|
|
706
|
+
if token.type in (
|
|
707
|
+
TokenType.STRING_LITERAL,
|
|
708
|
+
TokenType.INTEGER_LITERAL,
|
|
709
|
+
TokenType.FLOAT_LITERAL,
|
|
710
|
+
TokenType.BOOLEAN_LITERAL,
|
|
711
|
+
TokenType.NULL,
|
|
712
|
+
TokenType.PLACEHOLDER,
|
|
713
|
+
):
|
|
714
|
+
return CaseResult(literal=self._parse_value())
|
|
715
|
+
if token.type == TokenType.IDENTIFIER:
|
|
716
|
+
name = self._advance().value
|
|
717
|
+
table_alias = None
|
|
718
|
+
if self._match(TokenType.DOT):
|
|
719
|
+
self._advance()
|
|
720
|
+
table_alias = name
|
|
721
|
+
name = self._expect(TokenType.IDENTIFIER).value
|
|
722
|
+
return CaseResult(column=name, table_alias=table_alias)
|
|
723
|
+
raise SyntaxError_(f'Expected literal value, got {token.type.name}', token.position)
|
|
724
|
+
|
|
725
|
+
def _parse_join(self) -> JoinClause:
|
|
726
|
+
"""Parse a JOIN clause."""
|
|
727
|
+
join_type = 'INNER'
|
|
728
|
+
|
|
729
|
+
if self._match(TokenType.LEFT):
|
|
730
|
+
join_type = 'LEFT'
|
|
731
|
+
self._advance()
|
|
732
|
+
elif self._match(TokenType.RIGHT):
|
|
733
|
+
join_type = 'RIGHT'
|
|
734
|
+
self._advance()
|
|
735
|
+
elif self._match(TokenType.INNER):
|
|
736
|
+
self._advance()
|
|
737
|
+
|
|
738
|
+
self._expect(TokenType.JOIN)
|
|
739
|
+
right_table = self._expect(TokenType.IDENTIFIER).value
|
|
740
|
+
|
|
741
|
+
self._expect(TokenType.ON)
|
|
742
|
+
|
|
743
|
+
# Parse join condition: left_col = right_col
|
|
744
|
+
left_col = self._expect(TokenType.IDENTIFIER).value
|
|
745
|
+
left_table = None
|
|
746
|
+
if self._match(TokenType.DOT):
|
|
747
|
+
self._advance()
|
|
748
|
+
left_table = left_col
|
|
749
|
+
left_col = self._expect(TokenType.IDENTIFIER).value
|
|
750
|
+
|
|
751
|
+
self._expect(TokenType.EQUALS)
|
|
752
|
+
|
|
753
|
+
right_col = self._expect(TokenType.IDENTIFIER).value
|
|
754
|
+
right_table_alias = None
|
|
755
|
+
if self._match(TokenType.DOT):
|
|
756
|
+
self._advance()
|
|
757
|
+
right_table_alias = right_col
|
|
758
|
+
right_col = self._expect(TokenType.IDENTIFIER).value
|
|
759
|
+
|
|
760
|
+
return JoinClause(
|
|
761
|
+
table=right_table,
|
|
762
|
+
left_column=left_col,
|
|
763
|
+
right_column=right_col,
|
|
764
|
+
join_type=join_type,
|
|
765
|
+
left_table=left_table,
|
|
766
|
+
right_table=right_table_alias,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
def _parse_where(self) -> WhereClause:
|
|
770
|
+
"""Parse a WHERE clause with AND binding tighter than OR."""
|
|
771
|
+
return self._parse_or()
|
|
772
|
+
|
|
773
|
+
def _parse_or(self) -> WhereClause:
|
|
774
|
+
"""Parse OR-separated AND groups."""
|
|
775
|
+
conditions = [self._parse_and()]
|
|
776
|
+
while self._match(TokenType.OR):
|
|
777
|
+
self._advance()
|
|
778
|
+
conditions.append(self._parse_and())
|
|
779
|
+
if len(conditions) == 1:
|
|
780
|
+
if isinstance(conditions[0], WhereClause):
|
|
781
|
+
return conditions[0]
|
|
782
|
+
return WhereClause(conditions=conditions, operator='AND')
|
|
783
|
+
return WhereClause(conditions=conditions, operator='OR')
|
|
784
|
+
|
|
785
|
+
def _parse_and(self) -> Condition | WhereClause:
|
|
786
|
+
"""Parse AND-separated predicates."""
|
|
787
|
+
conditions = [self._parse_predicate()]
|
|
788
|
+
while self._match(TokenType.AND):
|
|
789
|
+
self._advance()
|
|
790
|
+
conditions.append(self._parse_predicate())
|
|
791
|
+
if len(conditions) == 1:
|
|
792
|
+
return conditions[0]
|
|
793
|
+
return WhereClause(conditions=conditions, operator='AND')
|
|
794
|
+
|
|
795
|
+
def _parse_predicate(self) -> Condition | WhereClause:
|
|
796
|
+
"""Parse a parenthesized clause or a single condition."""
|
|
797
|
+
if self._match(TokenType.LPAREN):
|
|
798
|
+
# Each pair recurses through OR and AND. Past this depth the
|
|
799
|
+
# call stack raises RecursionError instead of a SQL error.
|
|
800
|
+
if self._paren_depth >= 100:
|
|
801
|
+
token = self.tokens[self.pos]
|
|
802
|
+
raise SyntaxError_('Parentheses nested more than 100 levels', token.position)
|
|
803
|
+
self._advance()
|
|
804
|
+
self._paren_depth += 1
|
|
805
|
+
try:
|
|
806
|
+
clause = self._parse_or()
|
|
807
|
+
finally:
|
|
808
|
+
self._paren_depth -= 1
|
|
809
|
+
self._expect(TokenType.RPAREN)
|
|
810
|
+
return clause
|
|
811
|
+
return self._parse_condition()
|
|
812
|
+
|
|
813
|
+
def _parse_condition(self) -> Condition:
|
|
814
|
+
"""Parse a single condition."""
|
|
815
|
+
negated = False
|
|
816
|
+
if self._match(TokenType.NOT):
|
|
817
|
+
self._advance()
|
|
818
|
+
negated = True
|
|
819
|
+
|
|
820
|
+
# Parse column (possibly with table prefix or aggregate syntax)
|
|
821
|
+
table_alias = None
|
|
822
|
+
column = self._expect(TokenType.IDENTIFIER).value
|
|
823
|
+
|
|
824
|
+
if column.upper() in ('COUNT', 'SUM', 'AVG', 'MIN', 'MAX') and self._match(TokenType.LPAREN):
|
|
825
|
+
func = column.upper()
|
|
826
|
+
self._advance()
|
|
827
|
+
if self._match(TokenType.STAR):
|
|
828
|
+
self._advance()
|
|
829
|
+
agg_col = '*'
|
|
830
|
+
else:
|
|
831
|
+
agg_col = self._expect(TokenType.IDENTIFIER).value
|
|
832
|
+
if self._match(TokenType.DOT):
|
|
833
|
+
self._advance()
|
|
834
|
+
table_alias = agg_col
|
|
835
|
+
agg_col = self._expect(TokenType.IDENTIFIER).value
|
|
836
|
+
self._expect(TokenType.RPAREN)
|
|
837
|
+
shown = f'{table_alias}.{agg_col}' if table_alias else agg_col
|
|
838
|
+
column = f'{func}({shown})'
|
|
839
|
+
elif self._match(TokenType.DOT):
|
|
840
|
+
self._advance()
|
|
841
|
+
table_alias = column
|
|
842
|
+
column = self._expect(TokenType.IDENTIFIER).value
|
|
843
|
+
|
|
844
|
+
# Parse operator
|
|
845
|
+
if self._match(TokenType.IS):
|
|
846
|
+
self._advance()
|
|
847
|
+
if self._match(TokenType.NOT):
|
|
848
|
+
self._advance()
|
|
849
|
+
self._expect(TokenType.NULL)
|
|
850
|
+
return Condition(
|
|
851
|
+
column=column,
|
|
852
|
+
operator='IS NOT NULL',
|
|
853
|
+
value=None,
|
|
854
|
+
table_alias=table_alias,
|
|
855
|
+
negated=negated,
|
|
856
|
+
)
|
|
857
|
+
self._expect(TokenType.NULL)
|
|
858
|
+
return Condition(
|
|
859
|
+
column=column,
|
|
860
|
+
operator='IS NULL',
|
|
861
|
+
value=None,
|
|
862
|
+
table_alias=table_alias,
|
|
863
|
+
negated=negated,
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
if self._match(TokenType.EQUALS):
|
|
867
|
+
op = '='
|
|
868
|
+
self._advance()
|
|
869
|
+
elif self._match(TokenType.NOT_EQUALS):
|
|
870
|
+
op = '!='
|
|
871
|
+
self._advance()
|
|
872
|
+
elif self._match(TokenType.GREATER):
|
|
873
|
+
op = '>'
|
|
874
|
+
self._advance()
|
|
875
|
+
elif self._match(TokenType.GREATER_EQUAL):
|
|
876
|
+
op = '>='
|
|
877
|
+
self._advance()
|
|
878
|
+
elif self._match(TokenType.LESS):
|
|
879
|
+
op = '<'
|
|
880
|
+
self._advance()
|
|
881
|
+
elif self._match(TokenType.LESS_EQUAL):
|
|
882
|
+
op = '<='
|
|
883
|
+
self._advance()
|
|
884
|
+
elif self._match(TokenType.LIKE):
|
|
885
|
+
op = 'LIKE'
|
|
886
|
+
self._advance()
|
|
887
|
+
elif self._match(TokenType.IN):
|
|
888
|
+
op = 'IN'
|
|
889
|
+
self._advance()
|
|
890
|
+
else:
|
|
891
|
+
raise SyntaxError_(
|
|
892
|
+
f'Expected comparison operator, got {self._current().type.name}', self._current().position
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Parse value
|
|
896
|
+
if op == 'IN':
|
|
897
|
+
self._expect(TokenType.LPAREN)
|
|
898
|
+
values = []
|
|
899
|
+
while True:
|
|
900
|
+
values.append(self._parse_value())
|
|
901
|
+
if not self._match(TokenType.COMMA):
|
|
902
|
+
break
|
|
903
|
+
self._advance()
|
|
904
|
+
self._expect(TokenType.RPAREN)
|
|
905
|
+
value = values
|
|
906
|
+
else:
|
|
907
|
+
value = self._parse_value()
|
|
908
|
+
|
|
909
|
+
return Condition(column=column, operator=op, value=value, table_alias=table_alias, negated=negated)
|
|
910
|
+
|
|
911
|
+
def _parse_value(self) -> Any:
|
|
912
|
+
"""Parse a literal value."""
|
|
913
|
+
token = self._current()
|
|
914
|
+
|
|
915
|
+
if (
|
|
916
|
+
token.type == TokenType.STRING_LITERAL
|
|
917
|
+
or token.type == TokenType.INTEGER_LITERAL
|
|
918
|
+
or token.type == TokenType.FLOAT_LITERAL
|
|
919
|
+
or token.type == TokenType.BOOLEAN_LITERAL
|
|
920
|
+
):
|
|
921
|
+
self._advance()
|
|
922
|
+
return token.value
|
|
923
|
+
elif token.type == TokenType.NULL:
|
|
924
|
+
self._advance()
|
|
925
|
+
return None
|
|
926
|
+
elif token.type == TokenType.PLACEHOLDER:
|
|
927
|
+
self._advance()
|
|
928
|
+
return ParamPlaceholder()
|
|
929
|
+
else:
|
|
930
|
+
raise SyntaxError_(f'Expected literal value, got {token.type.name}', token.position)
|
|
931
|
+
|
|
932
|
+
def _parse_group_by(self) -> list[str]:
|
|
933
|
+
"""Parse GROUP BY columns. A qualifier is kept as 'table.column'."""
|
|
934
|
+
columns = []
|
|
935
|
+
while True:
|
|
936
|
+
name = self._expect(TokenType.IDENTIFIER).value
|
|
937
|
+
if self._match(TokenType.DOT):
|
|
938
|
+
self._advance()
|
|
939
|
+
name = f'{name}.{self._expect(TokenType.IDENTIFIER).value}'
|
|
940
|
+
columns.append(name)
|
|
941
|
+
if not self._match(TokenType.COMMA):
|
|
942
|
+
break
|
|
943
|
+
self._advance()
|
|
944
|
+
return columns
|
|
945
|
+
|
|
946
|
+
def _parse_order_by(self) -> list[OrderByItem]:
|
|
947
|
+
"""Parse ORDER BY clause."""
|
|
948
|
+
items = []
|
|
949
|
+
while True:
|
|
950
|
+
column = self._expect(TokenType.IDENTIFIER).value
|
|
951
|
+
table_alias = None
|
|
952
|
+
|
|
953
|
+
if self._match(TokenType.DOT):
|
|
954
|
+
self._advance()
|
|
955
|
+
table_alias = column
|
|
956
|
+
column = self._expect(TokenType.IDENTIFIER).value
|
|
957
|
+
|
|
958
|
+
direction = 'ASC'
|
|
959
|
+
if self._match(TokenType.ASC):
|
|
960
|
+
self._advance()
|
|
961
|
+
elif self._match(TokenType.DESC):
|
|
962
|
+
self._advance()
|
|
963
|
+
direction = 'DESC'
|
|
964
|
+
|
|
965
|
+
items.append(OrderByItem(column=column, direction=direction, table_alias=table_alias))
|
|
966
|
+
|
|
967
|
+
if not self._match(TokenType.COMMA):
|
|
968
|
+
break
|
|
969
|
+
self._advance()
|
|
970
|
+
|
|
971
|
+
return items
|
|
972
|
+
|
|
973
|
+
def _parse_insert(self) -> InsertQuery:
|
|
974
|
+
"""Parse an INSERT query."""
|
|
975
|
+
self._expect(TokenType.INSERT)
|
|
976
|
+
self._expect(TokenType.INTO)
|
|
977
|
+
|
|
978
|
+
table = self._expect(TokenType.IDENTIFIER).value
|
|
979
|
+
|
|
980
|
+
# Parse columns
|
|
981
|
+
self._expect(TokenType.LPAREN)
|
|
982
|
+
columns = []
|
|
983
|
+
while True:
|
|
984
|
+
columns.append(self._expect(TokenType.IDENTIFIER).value)
|
|
985
|
+
if not self._match(TokenType.COMMA):
|
|
986
|
+
break
|
|
987
|
+
self._advance()
|
|
988
|
+
self._expect(TokenType.RPAREN)
|
|
989
|
+
|
|
990
|
+
# Parse VALUES
|
|
991
|
+
self._expect(TokenType.VALUES)
|
|
992
|
+
self._expect(TokenType.LPAREN)
|
|
993
|
+
values = []
|
|
994
|
+
while True:
|
|
995
|
+
values.append(self._parse_value())
|
|
996
|
+
if not self._match(TokenType.COMMA):
|
|
997
|
+
break
|
|
998
|
+
self._advance()
|
|
999
|
+
self._expect(TokenType.RPAREN)
|
|
1000
|
+
|
|
1001
|
+
self._expect_end()
|
|
1002
|
+
|
|
1003
|
+
return InsertQuery(table=table, columns=columns, values=values)
|
|
1004
|
+
|
|
1005
|
+
def _parse_update(self) -> UpdateQuery:
|
|
1006
|
+
"""Parse an UPDATE query."""
|
|
1007
|
+
self._expect(TokenType.UPDATE)
|
|
1008
|
+
table = self._expect(TokenType.IDENTIFIER).value
|
|
1009
|
+
|
|
1010
|
+
# Parse SET clause
|
|
1011
|
+
self._expect(TokenType.SET)
|
|
1012
|
+
set_clause = {}
|
|
1013
|
+
while True:
|
|
1014
|
+
col = self._expect(TokenType.IDENTIFIER).value
|
|
1015
|
+
self._expect(TokenType.EQUALS)
|
|
1016
|
+
val = self._parse_value()
|
|
1017
|
+
if col in set_clause:
|
|
1018
|
+
raise InvalidQueryError(f"Duplicate column name '{col}'")
|
|
1019
|
+
set_clause[col] = val
|
|
1020
|
+
|
|
1021
|
+
if not self._match(TokenType.COMMA):
|
|
1022
|
+
break
|
|
1023
|
+
self._advance()
|
|
1024
|
+
|
|
1025
|
+
# Parse WHERE clause
|
|
1026
|
+
where = None
|
|
1027
|
+
if self._match(TokenType.WHERE):
|
|
1028
|
+
self._advance()
|
|
1029
|
+
where = self._parse_where()
|
|
1030
|
+
|
|
1031
|
+
self._expect_end()
|
|
1032
|
+
|
|
1033
|
+
return UpdateQuery(table=table, set_clause=set_clause, where=where)
|
|
1034
|
+
|
|
1035
|
+
def _parse_delete(self) -> DeleteQuery:
|
|
1036
|
+
"""Parse a DELETE query."""
|
|
1037
|
+
self._expect(TokenType.DELETE)
|
|
1038
|
+
self._expect(TokenType.FROM)
|
|
1039
|
+
|
|
1040
|
+
table = self._expect(TokenType.IDENTIFIER).value
|
|
1041
|
+
|
|
1042
|
+
# Parse WHERE clause
|
|
1043
|
+
where = None
|
|
1044
|
+
if self._match(TokenType.WHERE):
|
|
1045
|
+
self._advance()
|
|
1046
|
+
where = self._parse_where()
|
|
1047
|
+
|
|
1048
|
+
self._expect_end()
|
|
1049
|
+
|
|
1050
|
+
return DeleteQuery(table=table, where=where)
|
|
1051
|
+
|
|
1052
|
+
def _parse_create(self) -> CreateTableQuery:
|
|
1053
|
+
"""Parse a CREATE TABLE query."""
|
|
1054
|
+
self._expect(TokenType.CREATE)
|
|
1055
|
+
self._expect(TokenType.TABLE)
|
|
1056
|
+
|
|
1057
|
+
table = self._expect(TokenType.IDENTIFIER).value
|
|
1058
|
+
|
|
1059
|
+
# Parse column definitions
|
|
1060
|
+
self._expect(TokenType.LPAREN)
|
|
1061
|
+
columns = []
|
|
1062
|
+
while True:
|
|
1063
|
+
col = self._parse_column_def()
|
|
1064
|
+
columns.append(col)
|
|
1065
|
+
|
|
1066
|
+
if not self._match(TokenType.COMMA):
|
|
1067
|
+
break
|
|
1068
|
+
self._advance()
|
|
1069
|
+
self._expect(TokenType.RPAREN)
|
|
1070
|
+
|
|
1071
|
+
self._expect_end()
|
|
1072
|
+
|
|
1073
|
+
return CreateTableQuery(table=table, columns=columns)
|
|
1074
|
+
|
|
1075
|
+
def _parse_column_def(self) -> ColumnDef:
|
|
1076
|
+
"""Parse a column definition."""
|
|
1077
|
+
name = self._expect(TokenType.IDENTIFIER).value
|
|
1078
|
+
type_str = self._expect(TokenType.IDENTIFIER).value
|
|
1079
|
+
col_type = ColumnType.from_string(type_str)
|
|
1080
|
+
|
|
1081
|
+
primary_key = False
|
|
1082
|
+
nullable = True
|
|
1083
|
+
|
|
1084
|
+
# Check for PRIMARY KEY
|
|
1085
|
+
if self._match(TokenType.PRIMARY):
|
|
1086
|
+
self._advance()
|
|
1087
|
+
self._expect(TokenType.KEY)
|
|
1088
|
+
primary_key = True
|
|
1089
|
+
nullable = False
|
|
1090
|
+
|
|
1091
|
+
# Check for NULL/NOT NULL
|
|
1092
|
+
if self._match(TokenType.NOT):
|
|
1093
|
+
self._advance()
|
|
1094
|
+
self._expect(TokenType.NULL)
|
|
1095
|
+
nullable = False
|
|
1096
|
+
elif self._match(TokenType.NULL):
|
|
1097
|
+
self._advance()
|
|
1098
|
+
if primary_key:
|
|
1099
|
+
raise InvalidQueryError(f"PRIMARY KEY column '{name}' cannot be NULL")
|
|
1100
|
+
nullable = True
|
|
1101
|
+
|
|
1102
|
+
return ColumnDef(name=name, type=col_type, primary_key=primary_key, nullable=nullable)
|
|
1103
|
+
|
|
1104
|
+
def _parse_drop(self) -> DropTableQuery:
|
|
1105
|
+
"""Parse a DROP TABLE query."""
|
|
1106
|
+
self._expect(TokenType.DROP)
|
|
1107
|
+
self._expect(TokenType.TABLE)
|
|
1108
|
+
|
|
1109
|
+
table = self._expect(TokenType.IDENTIFIER).value
|
|
1110
|
+
|
|
1111
|
+
self._expect_end()
|
|
1112
|
+
|
|
1113
|
+
return DropTableQuery(table=table)
|
|
1114
|
+
|
|
1115
|
+
def _parse_begin(self) -> BeginQuery:
|
|
1116
|
+
"""Parse a BEGIN statement."""
|
|
1117
|
+
self._expect(TokenType.BEGIN)
|
|
1118
|
+
self._expect_end()
|
|
1119
|
+
return BeginQuery()
|
|
1120
|
+
|
|
1121
|
+
def _parse_commit(self) -> CommitQuery:
|
|
1122
|
+
"""Parse a COMMIT statement."""
|
|
1123
|
+
self._expect(TokenType.COMMIT)
|
|
1124
|
+
self._expect_end()
|
|
1125
|
+
return CommitQuery()
|
|
1126
|
+
|
|
1127
|
+
def _parse_rollback(self) -> RollbackQuery:
|
|
1128
|
+
"""Parse a ROLLBACK statement."""
|
|
1129
|
+
self._expect(TokenType.ROLLBACK)
|
|
1130
|
+
self._expect_end()
|
|
1131
|
+
return RollbackQuery()
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def parse_sql(
|
|
1135
|
+
sql: str,
|
|
1136
|
+
) -> (
|
|
1137
|
+
SelectQuery
|
|
1138
|
+
| InsertQuery
|
|
1139
|
+
| UpdateQuery
|
|
1140
|
+
| DeleteQuery
|
|
1141
|
+
| CreateTableQuery
|
|
1142
|
+
| DropTableQuery
|
|
1143
|
+
| BeginQuery
|
|
1144
|
+
| CommitQuery
|
|
1145
|
+
| RollbackQuery
|
|
1146
|
+
):
|
|
1147
|
+
"""Parse a SQL string into a query object."""
|
|
1148
|
+
lexer = Lexer(sql)
|
|
1149
|
+
tokens = lexer.tokenize()
|
|
1150
|
+
parser = Parser(tokens)
|
|
1151
|
+
return parser.parse()
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _count_placeholders_in_value(value: Any) -> int:
|
|
1155
|
+
if isinstance(value, ParamPlaceholder):
|
|
1156
|
+
return 1
|
|
1157
|
+
if isinstance(value, list):
|
|
1158
|
+
return sum(_count_placeholders_in_value(item) for item in value)
|
|
1159
|
+
return 0
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _count_where_placeholders(where: WhereClause | None) -> int:
|
|
1163
|
+
if where is None:
|
|
1164
|
+
return 0
|
|
1165
|
+
total = 0
|
|
1166
|
+
for cond in where.conditions:
|
|
1167
|
+
if isinstance(cond, Condition):
|
|
1168
|
+
total += _count_placeholders_in_value(cond.value)
|
|
1169
|
+
elif isinstance(cond, WhereClause):
|
|
1170
|
+
total += _count_where_placeholders(cond)
|
|
1171
|
+
return total
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _count_query_placeholders(
|
|
1175
|
+
query: (
|
|
1176
|
+
SelectQuery
|
|
1177
|
+
| InsertQuery
|
|
1178
|
+
| UpdateQuery
|
|
1179
|
+
| DeleteQuery
|
|
1180
|
+
| CreateTableQuery
|
|
1181
|
+
| DropTableQuery
|
|
1182
|
+
| BeginQuery
|
|
1183
|
+
| CommitQuery
|
|
1184
|
+
| RollbackQuery
|
|
1185
|
+
),
|
|
1186
|
+
) -> int:
|
|
1187
|
+
if isinstance(query, InsertQuery):
|
|
1188
|
+
return sum(_count_placeholders_in_value(value) for value in query.values)
|
|
1189
|
+
if isinstance(query, UpdateQuery):
|
|
1190
|
+
return sum(
|
|
1191
|
+
_count_placeholders_in_value(value) for value in query.set_clause.values()
|
|
1192
|
+
) + _count_where_placeholders(query.where)
|
|
1193
|
+
if isinstance(query, DeleteQuery):
|
|
1194
|
+
return _count_where_placeholders(query.where)
|
|
1195
|
+
if isinstance(query, SelectQuery):
|
|
1196
|
+
total = _count_where_placeholders(query.where) + _count_where_placeholders(query.having)
|
|
1197
|
+
for col in query.columns:
|
|
1198
|
+
if col.case is not None:
|
|
1199
|
+
total += _count_case_placeholders(col.case)
|
|
1200
|
+
return total
|
|
1201
|
+
return 0
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _count_case_placeholders(expr: CaseExpr) -> int:
|
|
1205
|
+
total = 0
|
|
1206
|
+
for when in expr.whens:
|
|
1207
|
+
total += _count_where_placeholders(when.condition)
|
|
1208
|
+
if when.result.column is None:
|
|
1209
|
+
total += _count_placeholders_in_value(when.result.literal)
|
|
1210
|
+
if expr.else_result is not None and expr.else_result.column is None:
|
|
1211
|
+
total += _count_placeholders_in_value(expr.else_result.literal)
|
|
1212
|
+
return total
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _bind_case(expr: CaseExpr, params: list[Any], index: list[int]) -> None:
|
|
1216
|
+
for when in expr.whens:
|
|
1217
|
+
_bind_where(when.condition, params, index)
|
|
1218
|
+
if when.result.column is None:
|
|
1219
|
+
when.result.literal = _bind_value(when.result.literal, params, index)
|
|
1220
|
+
if expr.else_result is not None and expr.else_result.column is None:
|
|
1221
|
+
expr.else_result.literal = _bind_value(expr.else_result.literal, params, index)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _bind_value(value: Any, params: list[Any], index: list[int]) -> Any:
|
|
1225
|
+
if isinstance(value, ParamPlaceholder):
|
|
1226
|
+
bound = params[index[0]]
|
|
1227
|
+
index[0] += 1
|
|
1228
|
+
return bound
|
|
1229
|
+
if isinstance(value, list):
|
|
1230
|
+
return [_bind_value(item, params, index) for item in value]
|
|
1231
|
+
return value
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _bind_where(where: WhereClause | None, params: list[Any], index: list[int]) -> None:
|
|
1235
|
+
if where is None:
|
|
1236
|
+
return
|
|
1237
|
+
for cond in where.conditions:
|
|
1238
|
+
if isinstance(cond, Condition):
|
|
1239
|
+
cond.value = _bind_value(cond.value, params, index)
|
|
1240
|
+
elif isinstance(cond, WhereClause):
|
|
1241
|
+
_bind_where(cond, params, index)
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def bind_params(
|
|
1245
|
+
query: (
|
|
1246
|
+
SelectQuery
|
|
1247
|
+
| InsertQuery
|
|
1248
|
+
| UpdateQuery
|
|
1249
|
+
| DeleteQuery
|
|
1250
|
+
| CreateTableQuery
|
|
1251
|
+
| DropTableQuery
|
|
1252
|
+
| BeginQuery
|
|
1253
|
+
| CommitQuery
|
|
1254
|
+
| RollbackQuery
|
|
1255
|
+
),
|
|
1256
|
+
params: Sequence[Any] | None = None,
|
|
1257
|
+
) -> None:
|
|
1258
|
+
"""Replace ParamPlaceholder sentinels left-to-right with bound values."""
|
|
1259
|
+
values = list(params) if params is not None else []
|
|
1260
|
+
expected = _count_query_placeholders(query)
|
|
1261
|
+
if expected != len(values):
|
|
1262
|
+
raise InvalidQueryError(f'expected {expected} parameters, got {len(values)}')
|
|
1263
|
+
|
|
1264
|
+
index = [0]
|
|
1265
|
+
if isinstance(query, InsertQuery):
|
|
1266
|
+
query.values = [_bind_value(value, values, index) for value in query.values]
|
|
1267
|
+
elif isinstance(query, UpdateQuery):
|
|
1268
|
+
for key in query.set_clause:
|
|
1269
|
+
query.set_clause[key] = _bind_value(query.set_clause[key], values, index)
|
|
1270
|
+
_bind_where(query.where, values, index)
|
|
1271
|
+
elif isinstance(query, DeleteQuery):
|
|
1272
|
+
_bind_where(query.where, values, index)
|
|
1273
|
+
elif isinstance(query, SelectQuery):
|
|
1274
|
+
for col in query.columns:
|
|
1275
|
+
if col.case is not None:
|
|
1276
|
+
_bind_case(col.case, values, index)
|
|
1277
|
+
_bind_where(query.where, values, index)
|
|
1278
|
+
_bind_where(query.having, values, index)
|