sqlo 0.0.1__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.
- sqlo/__init__.py +4 -0
- sqlo/builder.py +44 -0
- sqlo/constants.py +4 -0
- sqlo/dialects/base.py +29 -0
- sqlo/dialects/mysql.py +12 -0
- sqlo/expressions.py +504 -0
- sqlo/py.typed +0 -0
- sqlo/query/base.py +22 -0
- sqlo/query/delete.py +89 -0
- sqlo/query/insert.py +128 -0
- sqlo/query/mixins.py +279 -0
- sqlo/query/select.py +412 -0
- sqlo/query/update.py +129 -0
- sqlo/security.py +132 -0
- sqlo-0.0.1.dist-info/METADATA +88 -0
- sqlo-0.0.1.dist-info/RECORD +18 -0
- sqlo-0.0.1.dist-info/WHEEL +4 -0
- sqlo-0.0.1.dist-info/licenses/LICENSE +21 -0
sqlo/__init__.py
ADDED
sqlo/builder.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any, Union
|
|
2
|
+
|
|
3
|
+
from .dialects.mysql import MySQLDialect
|
|
4
|
+
from .expressions import Func, Raw
|
|
5
|
+
from .query.delete import DeleteQuery
|
|
6
|
+
from .query.insert import InsertQuery
|
|
7
|
+
from .query.select import SelectQuery
|
|
8
|
+
from .query.update import UpdateQuery
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Q:
|
|
12
|
+
"""Query Builder Factory."""
|
|
13
|
+
|
|
14
|
+
_default_dialect = MySQLDialect()
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def set_dialect(cls, dialect):
|
|
18
|
+
"""Set the default dialect for all queries."""
|
|
19
|
+
cls._default_dialect = dialect
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def get_dialect(cls):
|
|
23
|
+
"""Get the current default dialect."""
|
|
24
|
+
return cls._default_dialect
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def select(*columns: Union[str, Raw, Func]) -> SelectQuery:
|
|
28
|
+
return SelectQuery(*columns, dialect=Q._default_dialect)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def insert_into(table: str) -> InsertQuery:
|
|
32
|
+
return InsertQuery(table, dialect=Q._default_dialect)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def update(table: str) -> UpdateQuery:
|
|
36
|
+
return UpdateQuery(table, dialect=Q._default_dialect)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def delete_from(table: str) -> DeleteQuery:
|
|
40
|
+
return DeleteQuery(table, dialect=Q._default_dialect)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def raw(sql: str, params: Any = None) -> Raw:
|
|
44
|
+
return Raw(sql, params)
|
sqlo/constants.py
ADDED
sqlo/dialects/base.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Dialect(ABC):
|
|
5
|
+
"""Abstract base class for SQL dialects."""
|
|
6
|
+
|
|
7
|
+
@property
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def quote_char(self) -> str:
|
|
10
|
+
"""The character used to quote identifiers."""
|
|
11
|
+
|
|
12
|
+
def quote(self, identifier: str) -> str:
|
|
13
|
+
"""Quote an identifier."""
|
|
14
|
+
if "." in identifier:
|
|
15
|
+
parts = identifier.split(".")
|
|
16
|
+
return ".".join(
|
|
17
|
+
f"{self.quote_char}{part}{self.quote_char}" for part in parts
|
|
18
|
+
)
|
|
19
|
+
return f"{self.quote_char}{identifier}{self.quote_char}"
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def parameter_placeholder(self) -> str:
|
|
23
|
+
"""The placeholder for parameters (e.g., '?' or '%s')."""
|
|
24
|
+
|
|
25
|
+
def limit_offset(self, limit: int, offset: int) -> str:
|
|
26
|
+
"""Generate LIMIT and OFFSET clause."""
|
|
27
|
+
if offset > 0:
|
|
28
|
+
return f"LIMIT {limit} OFFSET {offset}"
|
|
29
|
+
return f"LIMIT {limit}"
|
sqlo/dialects/mysql.py
ADDED
sqlo/expressions.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
2
|
+
|
|
3
|
+
from .constants import COMPACT_PATTERN
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .query.select import SelectQuery
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Expression:
|
|
10
|
+
"""Base class for SQL expressions."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Raw(Expression):
|
|
14
|
+
"""Raw SQL fragment."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
sql: str,
|
|
19
|
+
params: Optional[Union[list[Any], tuple[Any, ...]]] = None,
|
|
20
|
+
):
|
|
21
|
+
self.sql = sql
|
|
22
|
+
self.params = params or []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Func(Expression):
|
|
26
|
+
"""
|
|
27
|
+
Represents a SQL function call.
|
|
28
|
+
|
|
29
|
+
This class allows you to wrap any SQL function (e.g., COUNT, MAX, AVG, custom functions)
|
|
30
|
+
and use it within your queries. It supports arguments and aliasing.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name (str): The name of the SQL function (e.g., "COUNT").
|
|
34
|
+
*args (Any): Arguments to pass to the function.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> f = Func("COUNT", "*")
|
|
38
|
+
>>> f.as_("total")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, name: str, *args: Any):
|
|
42
|
+
self.name = name
|
|
43
|
+
self.args = args
|
|
44
|
+
self.alias: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
def as_(self, alias: str) -> "Func":
|
|
47
|
+
self.alias = alias
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FunctionFactory:
|
|
52
|
+
"""Factory for creating SQL function expressions."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def __getattr__(self, name: str):
|
|
58
|
+
if name.startswith("_"):
|
|
59
|
+
# Avoid private attributes
|
|
60
|
+
raise AttributeError(
|
|
61
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _create(*args: Any) -> Func:
|
|
65
|
+
return Func(name.upper(), *args)
|
|
66
|
+
|
|
67
|
+
return _create
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def count(expression: str = "*") -> Func:
|
|
71
|
+
return Func("COUNT", expression)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def sum(expression: str) -> Func:
|
|
75
|
+
return Func("SUM", expression)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def avg(expression: str) -> Func:
|
|
79
|
+
return Func("AVG", expression)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def min(expression: str) -> Func:
|
|
83
|
+
return Func("MIN", expression)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def max(expression: str) -> Func:
|
|
87
|
+
return Func("MAX", expression)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Condition(Expression):
|
|
91
|
+
"""
|
|
92
|
+
Represents a SQL condition for use in WHERE or HAVING clauses.
|
|
93
|
+
|
|
94
|
+
This class allows you to build simple or complex conditions with support for
|
|
95
|
+
compact operator syntax (e.g., "age>") and standard syntax (e.g., "age >").
|
|
96
|
+
Conditions can be combined using bitwise operators (&, |) or static methods.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
column (str, optional): Column name with optional operator (e.g., "age>=" or "age >=").
|
|
100
|
+
value (Any, optional): The value to compare against. Can be a list for IN operator.
|
|
101
|
+
operator (str, optional): The comparison operator if not included in column. Defaults to "=".
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> # Simple condition
|
|
105
|
+
>>> c = Condition("age>=", 18)
|
|
106
|
+
>>> sql, params = c.build()
|
|
107
|
+
>>>
|
|
108
|
+
>>> # IN operator
|
|
109
|
+
>>> c = Condition("status", ["pending", "done"], operator="IN")
|
|
110
|
+
>>>
|
|
111
|
+
>>> # IS NULL
|
|
112
|
+
>>> c = Condition("deleted_at", operator="IS NULL")
|
|
113
|
+
>>>
|
|
114
|
+
>>> # Static factory methods
|
|
115
|
+
>>> c = Condition.null("deleted_at")
|
|
116
|
+
>>> c = Condition.in_("status", ["pending", "done"])
|
|
117
|
+
>>> c = Condition.exists(subquery)
|
|
118
|
+
>>>
|
|
119
|
+
>>> # Combining conditions
|
|
120
|
+
>>> c1 = Condition("age>=", 18)
|
|
121
|
+
>>> c2 = Condition("country", "US")
|
|
122
|
+
>>> combined = c1 & c2 # AND
|
|
123
|
+
>>> combined = c1 | c2 # OR
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
column: Optional[str] = None,
|
|
129
|
+
value: Any = None,
|
|
130
|
+
operator: str = "=",
|
|
131
|
+
):
|
|
132
|
+
from .dialects.mysql import MySQLDialect
|
|
133
|
+
|
|
134
|
+
self.parts: list[tuple[str, Any]] = [] # (sql, params)
|
|
135
|
+
self.connector = "AND"
|
|
136
|
+
self._dialect = MySQLDialect()
|
|
137
|
+
|
|
138
|
+
if column:
|
|
139
|
+
self._add_condition(column, value, operator)
|
|
140
|
+
|
|
141
|
+
def _add_condition(self, column: str, value: Any, operator: str) -> None:
|
|
142
|
+
"""Add a condition to this Condition object."""
|
|
143
|
+
col_name = None
|
|
144
|
+
op = None
|
|
145
|
+
|
|
146
|
+
# Parse column for embedded operator (e.g., "age>=")
|
|
147
|
+
if isinstance(column, str):
|
|
148
|
+
if " " in column:
|
|
149
|
+
parts = column.split(" ", 1)
|
|
150
|
+
col_name = parts[0]
|
|
151
|
+
op = parts[1]
|
|
152
|
+
else:
|
|
153
|
+
match = COMPACT_PATTERN.match(column)
|
|
154
|
+
if match:
|
|
155
|
+
col_name = match.group(1)
|
|
156
|
+
op = match.group(2)
|
|
157
|
+
|
|
158
|
+
# Use parsed operator if found
|
|
159
|
+
if col_name and op:
|
|
160
|
+
column = col_name
|
|
161
|
+
operator = op
|
|
162
|
+
|
|
163
|
+
# Handle IS NULL / IS NOT NULL
|
|
164
|
+
if operator.upper() in ("IS NULL", "IS NOT NULL"):
|
|
165
|
+
self.parts.append((f"`{column}` {operator.upper()}", []))
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# Handle IS / IS NOT with None value
|
|
169
|
+
if operator.upper() in ("IS", "IS NOT") and value is None:
|
|
170
|
+
null_op = "IS NULL" if operator.upper() == "IS" else "IS NOT NULL"
|
|
171
|
+
self.parts.append((f"`{column}` {null_op}", []))
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Handle IN / NOT IN operators
|
|
175
|
+
if operator.upper() in ("IN", "NOT IN"):
|
|
176
|
+
# Check if value is a subquery (has build method)
|
|
177
|
+
if hasattr(value, "build"):
|
|
178
|
+
sub_sql, sub_params = value.build()
|
|
179
|
+
self.parts.append(
|
|
180
|
+
(f"`{column}` {operator.upper()} ({sub_sql})", list(sub_params))
|
|
181
|
+
)
|
|
182
|
+
elif isinstance(value, (list, tuple)):
|
|
183
|
+
ph = self._dialect.parameter_placeholder()
|
|
184
|
+
placeholders = ", ".join([ph] * len(value))
|
|
185
|
+
self.parts.append(
|
|
186
|
+
(f"`{column}` {operator.upper()} ({placeholders})", list(value))
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"{operator} operator requires a list, tuple, or subquery"
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Handle Raw values (e.g., Raw("users.id") for column references)
|
|
195
|
+
if isinstance(value, Raw):
|
|
196
|
+
# Use raw SQL directly without placeholder
|
|
197
|
+
sql_fragment = f"`{column}` {operator} {value.sql}"
|
|
198
|
+
# Include Raw's params if any
|
|
199
|
+
self.parts.append((sql_fragment, list(value.params)))
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Handle standard operators with value
|
|
203
|
+
if value is not None:
|
|
204
|
+
self.parts.append(
|
|
205
|
+
(
|
|
206
|
+
f"`{column}` {operator} {self._dialect.parameter_placeholder()}",
|
|
207
|
+
[value],
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def build(self, dialect=None) -> tuple[str, tuple[Any, ...]]:
|
|
212
|
+
"""
|
|
213
|
+
Build the condition and return (sql, params) tuple.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
dialect: Optional dialect to use for quoting. Defaults to MySQLDialect.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Tuple of (sql_string, params_tuple)
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> c = Condition("age>=", 18)
|
|
223
|
+
>>> sql, params = c.build()
|
|
224
|
+
>>> assert sql == "`age` >= ?"
|
|
225
|
+
>>> assert params == (18,)
|
|
226
|
+
"""
|
|
227
|
+
if not self.parts:
|
|
228
|
+
return "", ()
|
|
229
|
+
|
|
230
|
+
parts_sql = []
|
|
231
|
+
params = []
|
|
232
|
+
|
|
233
|
+
for sql, p in self.parts:
|
|
234
|
+
if sql in ("AND", "OR"):
|
|
235
|
+
parts_sql.append(sql)
|
|
236
|
+
else:
|
|
237
|
+
parts_sql.append(sql)
|
|
238
|
+
if p:
|
|
239
|
+
params.extend(p)
|
|
240
|
+
|
|
241
|
+
sql = " ".join(parts_sql)
|
|
242
|
+
|
|
243
|
+
# Wrap in parentheses if multiple parts
|
|
244
|
+
if len(self.parts) > 1:
|
|
245
|
+
sql = f"({sql})"
|
|
246
|
+
|
|
247
|
+
return sql, tuple(params)
|
|
248
|
+
|
|
249
|
+
def __and__(self, other: "Condition") -> "Condition":
|
|
250
|
+
new_cond = Condition()
|
|
251
|
+
new_cond.parts = self.parts + [("AND", None)] + other.parts
|
|
252
|
+
return new_cond
|
|
253
|
+
|
|
254
|
+
def __or__(self, other: "Condition") -> "ComplexCondition":
|
|
255
|
+
# Use ComplexCondition for OR to properly handle precedence
|
|
256
|
+
return ComplexCondition("OR", self, other)
|
|
257
|
+
|
|
258
|
+
# Static factory methods
|
|
259
|
+
@staticmethod
|
|
260
|
+
def null(column: str) -> "Condition":
|
|
261
|
+
"""
|
|
262
|
+
Create an IS NULL condition.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
column: Column name to check for NULL
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Condition object
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
>>> c = Condition.null("deleted_at")
|
|
272
|
+
>>> sql, params = c.build()
|
|
273
|
+
>>> # SQL: `deleted_at` IS NULL
|
|
274
|
+
"""
|
|
275
|
+
return Condition(column, operator="IS NULL")
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def not_null(column: str) -> "Condition":
|
|
279
|
+
"""
|
|
280
|
+
Create an IS NOT NULL condition.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
column: Column name to check for NOT NULL
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Condition object
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> c = Condition.not_null("email")
|
|
290
|
+
>>> sql, params = c.build()
|
|
291
|
+
>>> # SQL: `email` IS NOT NULL
|
|
292
|
+
"""
|
|
293
|
+
return Condition(column, operator="IS NOT NULL")
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def in_(
|
|
297
|
+
column: str, values: Union[list[Any], tuple[Any, ...], "SelectQuery"]
|
|
298
|
+
) -> "Condition":
|
|
299
|
+
"""
|
|
300
|
+
Create an IN condition.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
column: Column name
|
|
304
|
+
values: List/tuple of values or a SelectQuery subquery
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Condition object
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
>>> c = Condition.in_("status", ["pending", "done"])
|
|
311
|
+
>>> sql, params = c.build()
|
|
312
|
+
>>> # SQL: `status` IN (?, ?)
|
|
313
|
+
"""
|
|
314
|
+
return Condition(column, values, operator="IN")
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def not_in(
|
|
318
|
+
column: str, values: Union[list[Any], tuple[Any, ...], "SelectQuery"]
|
|
319
|
+
) -> "Condition":
|
|
320
|
+
"""
|
|
321
|
+
Create a NOT IN condition.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
column: Column name
|
|
325
|
+
values: List/tuple of values or a SelectQuery subquery
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Condition object
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
>>> c = Condition.not_in("status", ["canceled", "failed"])
|
|
332
|
+
>>> sql, params = c.build()
|
|
333
|
+
>>> # SQL: `status` NOT IN (?, ?)
|
|
334
|
+
"""
|
|
335
|
+
return Condition(column, values, operator="NOT IN")
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def exists(subquery: "SelectQuery") -> "Condition":
|
|
339
|
+
"""
|
|
340
|
+
Create an EXISTS condition with a subquery.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
subquery: SelectQuery object
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Condition object
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> subquery = Q.select("1").from_("orders").where(Condition("user_id", 123))
|
|
350
|
+
>>> c = Condition.exists(subquery)
|
|
351
|
+
>>> sql, params = c.build()
|
|
352
|
+
>>> # SQL: EXISTS (SELECT 1 FROM `orders` WHERE `user_id` = ?)
|
|
353
|
+
"""
|
|
354
|
+
cond = Condition()
|
|
355
|
+
sub_sql, sub_params = subquery.build()
|
|
356
|
+
cond.parts.append((f"EXISTS ({sub_sql})", list(sub_params)))
|
|
357
|
+
return cond
|
|
358
|
+
|
|
359
|
+
@staticmethod
|
|
360
|
+
def not_exists(subquery: "SelectQuery") -> "Condition":
|
|
361
|
+
"""
|
|
362
|
+
Create a NOT EXISTS condition with a subquery.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
subquery: SelectQuery object
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Condition object
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
>>> subquery = Q.select("1").from_("orders").where(Condition("user_id", 123))
|
|
372
|
+
>>> c = Condition.not_exists(subquery)
|
|
373
|
+
>>> sql, params = c.build()
|
|
374
|
+
>>> # SQL: NOT EXISTS (SELECT 1 FROM `orders` WHERE `user_id` = ?)
|
|
375
|
+
"""
|
|
376
|
+
cond = Condition()
|
|
377
|
+
sub_sql, sub_params = subquery.build()
|
|
378
|
+
cond.parts.append((f"NOT EXISTS ({sub_sql})", list(sub_params)))
|
|
379
|
+
return cond
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def and_(*conditions: "Condition") -> "Condition":
|
|
383
|
+
"""
|
|
384
|
+
Combine multiple conditions with AND logic.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
*conditions: Variable number of Condition objects
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Combined Condition object
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
>>> c1 = Condition("age>=", 18)
|
|
394
|
+
>>> c2 = Condition("country", "US")
|
|
395
|
+
>>> c3 = Condition("verified", True)
|
|
396
|
+
>>> combined = Condition.and_(c1, c2, c3)
|
|
397
|
+
>>> # SQL: (`age` >= ? AND `country` = ? AND `verified` = ?)
|
|
398
|
+
"""
|
|
399
|
+
if not conditions:
|
|
400
|
+
return Condition()
|
|
401
|
+
|
|
402
|
+
result = conditions[0]
|
|
403
|
+
for cond in conditions[1:]:
|
|
404
|
+
result = result & cond
|
|
405
|
+
return result
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def or_(*conditions: "Condition") -> Union["Condition", "ComplexCondition"]:
|
|
409
|
+
"""
|
|
410
|
+
Combine multiple conditions with OR logic.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
*conditions: Variable number of Condition objects
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
ComplexCondition object
|
|
417
|
+
|
|
418
|
+
Example:
|
|
419
|
+
>>> c1 = Condition("status", "pending")
|
|
420
|
+
>>> c2 = Condition("status", "processing")
|
|
421
|
+
>>> combined = Condition.or_(c1, c2)
|
|
422
|
+
>>> # SQL: (`status` = ? OR `status` = ?)
|
|
423
|
+
"""
|
|
424
|
+
if not conditions:
|
|
425
|
+
return ComplexCondition("OR", Condition(), Condition())
|
|
426
|
+
|
|
427
|
+
result: Union[Condition, ComplexCondition] = conditions[0]
|
|
428
|
+
for cond in conditions[1:]:
|
|
429
|
+
result = result | cond
|
|
430
|
+
return result
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class ComplexCondition(Expression):
|
|
434
|
+
"""
|
|
435
|
+
Represents a complex SQL condition combining multiple conditions with AND/OR logic.
|
|
436
|
+
|
|
437
|
+
This class is typically created automatically when using bitwise operators (& or |)
|
|
438
|
+
on Condition objects, or when using Condition.and_() / Condition.or_() static methods.
|
|
439
|
+
It properly handles operator precedence and nested conditions.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
operator (str): The logical operator ("AND" or "OR").
|
|
443
|
+
left (Union[Condition, ComplexCondition]): The left-hand condition.
|
|
444
|
+
right (Union[Condition, ComplexCondition]): The right-hand condition.
|
|
445
|
+
|
|
446
|
+
Examples:
|
|
447
|
+
>>> c1 = Condition("age>=", 18)
|
|
448
|
+
>>> c2 = Condition("country", "US")
|
|
449
|
+
>>> c3 = Condition("verified", True)
|
|
450
|
+
>>>
|
|
451
|
+
>>> # Creates ComplexCondition automatically
|
|
452
|
+
>>> complex = (c1 & c2) | c3
|
|
453
|
+
>>> # Represents: (age >= 18 AND country = 'US') OR verified = True
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
def __init__(
|
|
457
|
+
self,
|
|
458
|
+
operator: str,
|
|
459
|
+
left: Union[Condition, "ComplexCondition"],
|
|
460
|
+
right: Union[Condition, "ComplexCondition"],
|
|
461
|
+
):
|
|
462
|
+
self.operator = operator
|
|
463
|
+
self.left = left
|
|
464
|
+
self.right = right
|
|
465
|
+
|
|
466
|
+
def build(self, dialect=None) -> tuple[str, tuple[Any, ...]]:
|
|
467
|
+
"""
|
|
468
|
+
Build the complex condition and return (sql, params) tuple.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
dialect: Optional dialect to use for quoting. Defaults to MySQLDialect.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Tuple of (sql_string, params_tuple)
|
|
475
|
+
|
|
476
|
+
Example:
|
|
477
|
+
>>> c1 = Condition("age>=", 18)
|
|
478
|
+
>>> c2 = Condition("country", "US")
|
|
479
|
+
>>> complex = c1 | c2
|
|
480
|
+
>>> sql, params = complex.build()
|
|
481
|
+
>>> # SQL: (`age` >= ? OR `country` = ?)
|
|
482
|
+
"""
|
|
483
|
+
# Build left and right conditions
|
|
484
|
+
left_sql, left_params = (
|
|
485
|
+
self.left.build(dialect) if hasattr(self.left, "build") else ("", ())
|
|
486
|
+
)
|
|
487
|
+
right_sql, right_params = (
|
|
488
|
+
self.right.build(dialect) if hasattr(self.right, "build") else ("", ())
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Combine with operator
|
|
492
|
+
sql = f"({left_sql} {self.operator} {right_sql})"
|
|
493
|
+
params = left_params + right_params
|
|
494
|
+
|
|
495
|
+
return sql, params
|
|
496
|
+
|
|
497
|
+
def __and__(self, other):
|
|
498
|
+
return ComplexCondition("AND", self, other)
|
|
499
|
+
|
|
500
|
+
def __or__(self, other):
|
|
501
|
+
return ComplexCondition("OR", self, other)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
func = FunctionFactory()
|
sqlo/py.typed
ADDED
|
File without changes
|
sqlo/query/base.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from ..dialects.base import Dialect
|
|
5
|
+
from ..dialects.mysql import MySQLDialect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Query(ABC):
|
|
9
|
+
"""Abstract base class for all queries."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, dialect: Optional[Dialect] = None):
|
|
12
|
+
self._dialect = dialect or MySQLDialect()
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def build(self) -> tuple[str, tuple[Any, ...]]:
|
|
16
|
+
"""Build the query and return (sql, params)."""
|
|
17
|
+
raise NotImplementedError("Subclasses must implement build()")
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
"""Return the compiled SQL string (for debugging)."""
|
|
21
|
+
sql, _ = self.build()
|
|
22
|
+
return sql
|