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 ADDED
@@ -0,0 +1,4 @@
1
+ from .builder import Q
2
+ from .expressions import Condition, Func, Raw, func
3
+
4
+ __all__ = ["Q", "Raw", "Func", "func", "Condition"]
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
@@ -0,0 +1,4 @@
1
+ import re
2
+
3
+ # Pre-compile regex for compact operators (e.g. "age>=")
4
+ COMPACT_PATTERN = re.compile(r"^([\w\.]+)([<>=!]+)$")
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
@@ -0,0 +1,12 @@
1
+ from .base import Dialect
2
+
3
+
4
+ class MySQLDialect(Dialect):
5
+ """MySQL dialect implementation."""
6
+
7
+ @property
8
+ def quote_char(self) -> str:
9
+ return "`"
10
+
11
+ def parameter_placeholder(self) -> str:
12
+ return "%s"
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