ormlambda 3.35.3__py3-none-any.whl → 4.0.4__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.
- ormlambda/__init__.py +79 -51
- ormlambda/caster/caster.py +6 -1
- ormlambda/common/abstract_classes/__init__.py +0 -2
- ormlambda/common/enums/__init__.py +1 -0
- ormlambda/common/enums/order_type.py +9 -0
- ormlambda/common/errors/__init__.py +13 -3
- ormlambda/common/global_checker.py +86 -8
- ormlambda/common/interfaces/IQueryCommand.py +2 -2
- ormlambda/common/interfaces/__init__.py +0 -2
- ormlambda/dialects/__init__.py +75 -3
- ormlambda/dialects/default/base.py +1 -1
- ormlambda/dialects/mysql/__init__.py +35 -78
- ormlambda/dialects/mysql/base.py +226 -40
- ormlambda/dialects/mysql/clauses/ST_AsText.py +26 -0
- ormlambda/dialects/mysql/clauses/ST_Contains.py +30 -0
- ormlambda/dialects/mysql/clauses/__init__.py +1 -0
- ormlambda/dialects/mysql/repository/__init__.py +1 -0
- ormlambda/{databases/my_sql → dialects/mysql/repository}/repository.py +0 -5
- ormlambda/dialects/mysql/types.py +6 -0
- ormlambda/engine/base.py +26 -4
- ormlambda/errors.py +9 -0
- ormlambda/model/base_model.py +3 -10
- ormlambda/repository/base_repository.py +1 -1
- ormlambda/repository/interfaces/IRepositoryBase.py +0 -7
- ormlambda/repository/response.py +21 -8
- ormlambda/sql/__init__.py +12 -3
- ormlambda/sql/clause_info/__init__.py +0 -2
- ormlambda/sql/clause_info/clause_info.py +94 -76
- ormlambda/sql/clause_info/interface/IAggregate.py +14 -4
- ormlambda/sql/clause_info/interface/IClauseInfo.py +6 -11
- ormlambda/sql/clauses/alias.py +6 -37
- ormlambda/sql/clauses/count.py +21 -36
- ormlambda/sql/clauses/group_by.py +13 -19
- ormlambda/sql/clauses/having.py +2 -6
- ormlambda/sql/clauses/insert.py +3 -3
- ormlambda/sql/clauses/interfaces/__init__.py +0 -1
- ormlambda/sql/clauses/join/join_context.py +5 -12
- ormlambda/sql/clauses/joins.py +34 -52
- ormlambda/sql/clauses/limit.py +1 -2
- ormlambda/sql/clauses/offset.py +1 -2
- ormlambda/sql/clauses/order.py +17 -21
- ormlambda/sql/clauses/select.py +56 -28
- ormlambda/sql/clauses/update.py +13 -10
- ormlambda/sql/clauses/where.py +20 -39
- ormlambda/sql/column/__init__.py +1 -0
- ormlambda/sql/column/column.py +19 -12
- ormlambda/sql/column/column_proxy.py +117 -0
- ormlambda/sql/column_table_proxy.py +23 -0
- ormlambda/sql/comparer.py +31 -65
- ormlambda/sql/compiler.py +248 -58
- ormlambda/sql/context/__init__.py +304 -0
- ormlambda/sql/ddl.py +19 -5
- ormlambda/sql/elements.py +3 -0
- ormlambda/sql/foreign_key.py +42 -64
- ormlambda/sql/functions/__init__.py +0 -1
- ormlambda/sql/functions/concat.py +35 -38
- ormlambda/sql/functions/max.py +12 -36
- ormlambda/sql/functions/min.py +13 -28
- ormlambda/sql/functions/sum.py +17 -33
- ormlambda/sql/sqltypes.py +2 -0
- ormlambda/sql/table/__init__.py +1 -0
- ormlambda/sql/table/table.py +31 -45
- ormlambda/sql/table/table_proxy.py +88 -0
- ormlambda/sql/type_api.py +4 -1
- ormlambda/sql/types.py +15 -12
- ormlambda/statements/__init__.py +0 -2
- ormlambda/statements/base_statement.py +53 -91
- ormlambda/statements/interfaces/IStatements.py +77 -123
- ormlambda/statements/interfaces/__init__.py +1 -1
- ormlambda/statements/query_builder.py +296 -128
- ormlambda/statements/statements.py +122 -115
- ormlambda/statements/types.py +5 -25
- ormlambda/util/__init__.py +7 -100
- ormlambda/util/langhelpers.py +102 -0
- ormlambda/util/module_tree/dynamic_module.py +1 -1
- ormlambda/util/preloaded.py +80 -0
- ormlambda/util/typing.py +12 -3
- {ormlambda-3.35.3.dist-info → ormlambda-4.0.4.dist-info}/METADATA +56 -79
- ormlambda-4.0.4.dist-info/RECORD +139 -0
- ormlambda/common/abstract_classes/clause_info_converter.py +0 -65
- ormlambda/common/abstract_classes/decomposition_query.py +0 -141
- ormlambda/common/abstract_classes/query_base.py +0 -15
- ormlambda/common/interfaces/ICustomAlias.py +0 -7
- ormlambda/common/interfaces/IDecompositionQuery.py +0 -33
- ormlambda/databases/__init__.py +0 -4
- ormlambda/databases/my_sql/__init__.py +0 -3
- ormlambda/databases/my_sql/clauses/ST_AsText.py +0 -37
- ormlambda/databases/my_sql/clauses/ST_Contains.py +0 -36
- ormlambda/databases/my_sql/clauses/__init__.py +0 -14
- ormlambda/databases/my_sql/clauses/count.py +0 -33
- ormlambda/databases/my_sql/clauses/delete.py +0 -9
- ormlambda/databases/my_sql/clauses/drop_table.py +0 -26
- ormlambda/databases/my_sql/clauses/group_by.py +0 -17
- ormlambda/databases/my_sql/clauses/having.py +0 -12
- ormlambda/databases/my_sql/clauses/insert.py +0 -9
- ormlambda/databases/my_sql/clauses/joins.py +0 -14
- ormlambda/databases/my_sql/clauses/limit.py +0 -6
- ormlambda/databases/my_sql/clauses/offset.py +0 -6
- ormlambda/databases/my_sql/clauses/order.py +0 -8
- ormlambda/databases/my_sql/clauses/update.py +0 -8
- ormlambda/databases/my_sql/clauses/upsert.py +0 -9
- ormlambda/databases/my_sql/clauses/where.py +0 -7
- ormlambda/dialects/interface/__init__.py +0 -1
- ormlambda/dialects/interface/dialect.py +0 -78
- ormlambda/sql/clause_info/aggregate_function_base.py +0 -96
- ormlambda/sql/clause_info/clause_info_context.py +0 -87
- ormlambda/sql/clauses/interfaces/ISelect.py +0 -17
- ormlambda/sql/clauses/new_join.py +0 -119
- ormlambda/util/load_module.py +0 -21
- ormlambda/util/plugin_loader.py +0 -32
- ormlambda-3.35.3.dist-info/RECORD +0 -159
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/__init__.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/caster.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/__init__.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/boolean.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/bytes.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/date.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/datetime.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/decimal.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/float.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/int.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/iterable.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/json.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/none.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/point.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/string.py +0 -0
- /ormlambda/{databases/my_sql → dialects/mysql/repository}/pool_types.py +0 -0
- {ormlambda-3.35.3.dist-info → ormlambda-4.0.4.dist-info}/AUTHORS +0 -0
- {ormlambda-3.35.3.dist-info → ormlambda-4.0.4.dist-info}/LICENSE +0 -0
- {ormlambda-3.35.3.dist-info → ormlambda-4.0.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import TYPE_CHECKING, Optional, overload
|
3
|
+
|
4
|
+
|
5
|
+
from ormlambda.sql.column_table_proxy import ColumnTableProxy
|
6
|
+
from .column import Column
|
7
|
+
from ormlambda import ConditionType, JoinType
|
8
|
+
from ormlambda.sql.elements import ClauseElement
|
9
|
+
from ormlambda import util
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from ormlambda.sql.types import ColumnType, ComparerType
|
13
|
+
from ormlambda.sql.elements import Dialect
|
14
|
+
from ormlambda.sql.context import FKChain
|
15
|
+
from ormlambda.sql.comparer import Comparer
|
16
|
+
from ormlambda.sql.clauses import JoinSelector
|
17
|
+
|
18
|
+
|
19
|
+
class ColumnProxy[TProp](ColumnTableProxy, Column[TProp], ClauseElement):
|
20
|
+
__visit_name__ = "column_proxy"
|
21
|
+
_column: Column
|
22
|
+
_path: FKChain
|
23
|
+
alias: Optional[str]
|
24
|
+
|
25
|
+
@overload
|
26
|
+
def __init__(self, column: Column[TProp], path: FKChain): ...
|
27
|
+
@overload
|
28
|
+
def __init__(self, column: Column[TProp], path: FKChain, alias: str): ...
|
29
|
+
|
30
|
+
def __init__(self, column, path, alias=None):
|
31
|
+
self._column = column
|
32
|
+
self.alias = alias
|
33
|
+
super().__init__(path)
|
34
|
+
|
35
|
+
def __str__(self) -> str:
|
36
|
+
return self.get_full_chain()
|
37
|
+
|
38
|
+
def __repr__(self) -> str:
|
39
|
+
table = self._column.table
|
40
|
+
table = table.__table_name__ if table else None
|
41
|
+
|
42
|
+
col = f"{table}.{self._column.column_name}"
|
43
|
+
path = self._path.get_path_key()
|
44
|
+
|
45
|
+
return f"{ColumnProxy.__name__}({col if table else self._column.column_name}) {f'Path={path}' if path else ""}"
|
46
|
+
|
47
|
+
def __getattr__(self, name: str):
|
48
|
+
# it does not work when comparing methods
|
49
|
+
return getattr(self._column, name)
|
50
|
+
|
51
|
+
@util.preload_module("ormlambda.sql.comparer")
|
52
|
+
def __comparer_creator(self, other: ColumnType, compare: ComparerType) -> Comparer:
|
53
|
+
Comparer = util.preloaded.sql_comparer.Comparer
|
54
|
+
|
55
|
+
return Comparer(self, other, compare)
|
56
|
+
|
57
|
+
def __eq__(self, other: ColumnType) -> Comparer:
|
58
|
+
return self.__comparer_creator(other, ConditionType.EQUAL.value)
|
59
|
+
|
60
|
+
def __ne__(self, other: ColumnType) -> Comparer:
|
61
|
+
return self.__comparer_creator(other, ConditionType.NOT_EQUAL.value)
|
62
|
+
|
63
|
+
def __lt__(self, other: ColumnType) -> Comparer:
|
64
|
+
return self.__comparer_creator(other, ConditionType.LESS_THAN.value)
|
65
|
+
|
66
|
+
def __le__(self, other: ColumnType) -> Comparer:
|
67
|
+
return self.__comparer_creator(other, ConditionType.LESS_THAN_OR_EQUAL.value)
|
68
|
+
|
69
|
+
def __gt__(self, other: ColumnType) -> Comparer:
|
70
|
+
return self.__comparer_creator(other, ConditionType.GREATER_THAN.value)
|
71
|
+
|
72
|
+
def __ge__(self, other: ColumnType) -> Comparer:
|
73
|
+
return self.__comparer_creator(other, ConditionType.GREATER_THAN_OR_EQUAL.value)
|
74
|
+
|
75
|
+
def get_full_chain(self, chr: str = "."):
|
76
|
+
alias: list[str] = [self._path.base.__table_name__]
|
77
|
+
|
78
|
+
n = self.number_table_in_chain()
|
79
|
+
|
80
|
+
for i in range(n):
|
81
|
+
fk = self._path.steps[i]
|
82
|
+
|
83
|
+
value = fk.clause_name
|
84
|
+
alias.append(value)
|
85
|
+
|
86
|
+
# Column name
|
87
|
+
alias.append(self._column.column_name)
|
88
|
+
return chr.join(alias)
|
89
|
+
|
90
|
+
def get_table_chain(self) -> str:
|
91
|
+
return self._path.get_alias()
|
92
|
+
|
93
|
+
def number_table_in_chain(self) -> int:
|
94
|
+
return len(self._path.steps)
|
95
|
+
|
96
|
+
@util.preload_module("ormlambda.sql.clauses")
|
97
|
+
def get_relations(self, by: JoinType, dialect: Dialect) -> tuple[JoinSelector]:
|
98
|
+
JoinSelector = util.preloaded.sql_clauses.JoinSelector
|
99
|
+
|
100
|
+
result: list[JoinSelector] = []
|
101
|
+
alias = self._path.base.__table_name__
|
102
|
+
|
103
|
+
for i in range(self.number_table_in_chain()):
|
104
|
+
tbl = self._path.steps[i]
|
105
|
+
relation = tbl.resolved_function()
|
106
|
+
|
107
|
+
relation.left_condition.alias = alias
|
108
|
+
relation.left_condition.path = self._path[:i]
|
109
|
+
|
110
|
+
alias += f"_{tbl.clause_name}"
|
111
|
+
|
112
|
+
relation.right_condition.alias = alias
|
113
|
+
relation.right_condition.path = self._path[: i + 1]
|
114
|
+
|
115
|
+
js = JoinSelector(relation, by, alias, dialect=dialect)
|
116
|
+
result.append(js)
|
117
|
+
return result
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import abc
|
3
|
+
|
4
|
+
from ormlambda.sql.context import FKChain
|
5
|
+
|
6
|
+
|
7
|
+
class ColumnTableProxy(abc.ABC):
|
8
|
+
_path: FKChain
|
9
|
+
|
10
|
+
def __init__(self, path: FKChain):
|
11
|
+
self._path = path
|
12
|
+
|
13
|
+
@property
|
14
|
+
def path(self) -> FKChain:
|
15
|
+
return self._path
|
16
|
+
|
17
|
+
@path.setter
|
18
|
+
def path(self, value: FKChain) -> None:
|
19
|
+
self._path = value
|
20
|
+
return None
|
21
|
+
|
22
|
+
@abc.abstractmethod
|
23
|
+
def get_table_chain(self): ...
|
ormlambda/sql/comparer.py
CHANGED
@@ -2,17 +2,16 @@ from __future__ import annotations
|
|
2
2
|
import abc
|
3
3
|
import re
|
4
4
|
import typing as tp
|
5
|
-
from ormlambda.common.interfaces.IQueryCommand import IQuery
|
6
5
|
|
7
6
|
|
8
|
-
from ormlambda.sql.types import ConditionType
|
9
|
-
from ormlambda.sql.
|
7
|
+
from ormlambda.sql.types import ConditionType
|
8
|
+
from ormlambda.sql.types import ComparerTypes
|
9
|
+
|
10
|
+
from ormlambda.sql.clause_info import IAggregate
|
10
11
|
from ormlambda import ConditionType as ConditionEnum
|
11
|
-
from ormlambda.sql.elements import
|
12
|
+
from ormlambda.sql.elements import ClauseElement
|
12
13
|
|
13
|
-
|
14
|
-
from ormlambda.sql.clause_info.clause_info_context import ClauseContextType
|
15
|
-
from ormlambda.dialects import Dialect
|
14
|
+
from ormlambda import ColumnProxy
|
16
15
|
|
17
16
|
|
18
17
|
class ICleaner(abc.ABC):
|
@@ -46,7 +45,7 @@ class CleanValue:
|
|
46
45
|
return temp_name
|
47
46
|
|
48
47
|
|
49
|
-
class Comparer(
|
48
|
+
class Comparer(ClauseElement, IAggregate):
|
50
49
|
__visit_name__ = "comparer"
|
51
50
|
|
52
51
|
def __init__(
|
@@ -54,64 +53,23 @@ class Comparer(Element, IQuery):
|
|
54
53
|
left_condition: ConditionType,
|
55
54
|
right_condition: ConditionType,
|
56
55
|
compare: ComparerTypes,
|
57
|
-
context: ClauseContextType = None,
|
58
56
|
flags: tp.Optional[tp.Iterable[re.RegexFlag]] = None,
|
59
|
-
dialect: tp.Optional[Dialect] = None,
|
60
57
|
) -> None:
|
61
|
-
self._context: ClauseContextType = context
|
62
58
|
self._compare: ComparerTypes = compare
|
63
|
-
self.
|
64
|
-
self.
|
59
|
+
self.left_condition: ConditionType = left_condition
|
60
|
+
self.right_condition: ConditionType = right_condition
|
65
61
|
self._flags = flags
|
66
|
-
self._dialect = dialect
|
67
|
-
|
68
|
-
def set_context(self, context: ClauseContextType) -> None:
|
69
|
-
self._context = context
|
70
|
-
return None
|
71
62
|
|
72
|
-
|
73
|
-
|
74
|
-
return None
|
63
|
+
@property
|
64
|
+
def dtype(self) -> tp.Any: ...
|
75
65
|
|
76
66
|
def __repr__(self) -> str:
|
77
|
-
return f"{Comparer.__name__}: {self.
|
78
|
-
|
79
|
-
def _create_clause_info(self, cond: ConditionType, dialect: Dialect, **kw) -> Comparer | ClauseInfo:
|
80
|
-
from ormlambda import Column
|
81
|
-
|
82
|
-
if isinstance(cond, Comparer):
|
83
|
-
return cond
|
84
|
-
table = None if not isinstance(cond, Column) else cond.table
|
85
|
-
|
86
|
-
# it a value that's not depend of any Table
|
87
|
-
return ClauseInfo(
|
88
|
-
table,
|
89
|
-
cond,
|
90
|
-
alias_clause=None,
|
91
|
-
context=self._context,
|
92
|
-
dialect=dialect,
|
93
|
-
**kw,
|
94
|
-
)
|
95
|
-
|
96
|
-
def left_condition(self, dialect: Dialect) -> Comparer | ClauseInfo:
|
97
|
-
return self._create_clause_info(self._left_condition, dialect=dialect)
|
98
|
-
|
99
|
-
def right_condition(self, dialect: Dialect) -> Comparer | ClauseInfo:
|
100
|
-
return self._create_clause_info(self._right_condition, dialect=dialect)
|
67
|
+
return f"{Comparer.__name__}: {self.left_condition} {self._compare} {self.right_condition}"
|
101
68
|
|
102
69
|
@property
|
103
70
|
def compare(self) -> ComparerTypes:
|
104
71
|
return self._compare
|
105
72
|
|
106
|
-
def query(self, dialect: Dialect, **kwargs) -> str:
|
107
|
-
lcond = self.left_condition(dialect).query(dialect, **kwargs)
|
108
|
-
rcond = self.right_condition(dialect).query(dialect, **kwargs)
|
109
|
-
|
110
|
-
if self._flags:
|
111
|
-
rcond = CleanValue(rcond, self._flags).clean()
|
112
|
-
|
113
|
-
return f"{lcond} {self._compare} {rcond}"
|
114
|
-
|
115
73
|
def __and__(self, other: Comparer, **kwargs) -> Comparer:
|
116
74
|
# Customize the behavior of '&'
|
117
75
|
return Comparer(self, other, "AND", **kwargs)
|
@@ -121,13 +79,13 @@ class Comparer(Element, IQuery):
|
|
121
79
|
return Comparer(self, other, "OR", **kwargs)
|
122
80
|
|
123
81
|
@classmethod
|
124
|
-
def join_comparers(cls, comparers: list[Comparer], restrictive: bool = True,
|
82
|
+
def join_comparers(cls, comparers: list[Comparer], restrictive: bool = True, *, dialect, **kwargs) -> str:
|
125
83
|
if not isinstance(comparers, tp.Iterable):
|
126
84
|
raise ValueError(f"Excepted '{Comparer.__name__}' iterable not {type(comparers).__name__}")
|
85
|
+
|
127
86
|
if len(comparers) == 1:
|
128
87
|
comparer = comparers[0]
|
129
|
-
comparer.
|
130
|
-
return comparer.query(dialect)
|
88
|
+
return comparer.compile(dialect, alias_clause=None, **kwargs).string
|
131
89
|
|
132
90
|
join_method = cls.__or__ if not restrictive else cls.__and__
|
133
91
|
|
@@ -135,12 +93,23 @@ class Comparer(Element, IQuery):
|
|
135
93
|
for i in range(len(comparers) - 1):
|
136
94
|
if ini_comparer is None:
|
137
95
|
ini_comparer = comparers[i]
|
138
|
-
ini_comparer._context = context
|
139
96
|
right_comparer = comparers[i + 1]
|
140
|
-
|
141
|
-
new_comparer = join_method(ini_comparer, right_comparer, context=context, dialect=dialect)
|
97
|
+
new_comparer = join_method(ini_comparer, right_comparer)
|
142
98
|
ini_comparer = new_comparer
|
143
|
-
|
99
|
+
|
100
|
+
res = new_comparer.compile(dialect, alias_clause=None, **kwargs)
|
101
|
+
return res.string
|
102
|
+
|
103
|
+
def used_columns(self) -> tp.Iterable[ColumnProxy]:
|
104
|
+
res = []
|
105
|
+
|
106
|
+
if isinstance(self.left_condition, ColumnProxy):
|
107
|
+
res.append(self.left_condition)
|
108
|
+
|
109
|
+
if isinstance(self.right_condition, ColumnProxy):
|
110
|
+
res.append(self.right_condition)
|
111
|
+
|
112
|
+
return res
|
144
113
|
|
145
114
|
|
146
115
|
class Regex(Comparer):
|
@@ -148,14 +117,12 @@ class Regex(Comparer):
|
|
148
117
|
self,
|
149
118
|
left_condition: ConditionType,
|
150
119
|
right_condition: ConditionType,
|
151
|
-
context: ClauseContextType = None,
|
152
120
|
flags: tp.Optional[tp.Iterable[re.RegexFlag]] = None,
|
153
121
|
):
|
154
122
|
super().__init__(
|
155
123
|
left_condition=left_condition,
|
156
124
|
right_condition=right_condition,
|
157
125
|
compare=ConditionEnum.REGEXP.value,
|
158
|
-
context=context,
|
159
126
|
flags=flags, # Pass as a named parameter instead
|
160
127
|
)
|
161
128
|
|
@@ -165,6 +132,5 @@ class Like(Comparer):
|
|
165
132
|
self,
|
166
133
|
left_condition: ConditionType,
|
167
134
|
right_condition: ConditionType,
|
168
|
-
context: ClauseContextType = None,
|
169
135
|
):
|
170
|
-
super().__init__(left_condition, right_condition, ConditionEnum.LIKE.value
|
136
|
+
super().__init__(left_condition, right_condition, ConditionEnum.LIKE.value)
|
ormlambda/sql/compiler.py
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import
|
3
|
-
|
2
|
+
import datetime
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
from pathlib import Path
|
7
|
+
import subprocess
|
8
|
+
import sys
|
9
|
+
from typing import Any, BinaryIO, ClassVar, Optional, TYPE_CHECKING, TextIO, Union
|
4
10
|
|
5
11
|
from ormlambda.sql.ddl import CreateColumn
|
6
|
-
from ormlambda.sql.foreign_key import ForeignKey
|
7
12
|
from ormlambda.sql.sqltypes import resolve_primitive_types
|
8
13
|
|
9
14
|
from .visitors import Visitor
|
10
|
-
from ormlambda import util
|
11
15
|
from ormlambda.sql.type_api import TypeEngine
|
12
16
|
|
13
17
|
if TYPE_CHECKING:
|
@@ -15,7 +19,13 @@ if TYPE_CHECKING:
|
|
15
19
|
from .visitors import Element
|
16
20
|
from .elements import ClauseElement
|
17
21
|
from ormlambda.dialects import Dialect
|
18
|
-
from ormlambda.sql.ddl import
|
22
|
+
from ormlambda.sql.ddl import (
|
23
|
+
CreateTable,
|
24
|
+
CreateSchema,
|
25
|
+
DropSchema,
|
26
|
+
DropTable,
|
27
|
+
CreateBackup,
|
28
|
+
)
|
19
29
|
from .sqltypes import (
|
20
30
|
INTEGER,
|
21
31
|
SMALLINTEGER,
|
@@ -43,23 +53,10 @@ if TYPE_CHECKING:
|
|
43
53
|
POINT,
|
44
54
|
)
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
Update,
|
51
|
-
Limit,
|
52
|
-
Offset,
|
53
|
-
Count,
|
54
|
-
Where,
|
55
|
-
Having,
|
56
|
-
Order,
|
57
|
-
Concat,
|
58
|
-
Max,
|
59
|
-
Min,
|
60
|
-
Sum,
|
61
|
-
Groupby,
|
62
|
-
)
|
56
|
+
type customString = Union[str | Path]
|
57
|
+
|
58
|
+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
59
|
+
log = logging.getLogger(__name__)
|
63
60
|
|
64
61
|
|
65
62
|
class Compiled:
|
@@ -141,40 +138,9 @@ class TypeCompiler(Visitor):
|
|
141
138
|
return type_._compiler_dispatch(self, **kw)
|
142
139
|
|
143
140
|
|
144
|
-
class SQLCompiler(Compiled
|
141
|
+
class SQLCompiler(Compiled):
|
145
142
|
is_sql = True
|
146
143
|
|
147
|
-
@abc.abstractmethod
|
148
|
-
def visit_insert(self, insert: Insert, **kw) -> Insert: ...
|
149
|
-
@abc.abstractmethod
|
150
|
-
def visit_delete(self, delete: Delete, **kw) -> Delete: ...
|
151
|
-
@abc.abstractmethod
|
152
|
-
def visit_upsert(self, upsert: Upsert, **kw) -> Upsert: ...
|
153
|
-
@abc.abstractmethod
|
154
|
-
def visit_update(self, update: Update, **kw) -> Update: ...
|
155
|
-
@abc.abstractmethod
|
156
|
-
def visit_limit(self, limit: Limit, **kw) -> Limit: ...
|
157
|
-
@abc.abstractmethod
|
158
|
-
def visit_offset(self, offset: Offset, **kw) -> Offset: ...
|
159
|
-
@abc.abstractmethod
|
160
|
-
def visit_count(self, count: Count, **kw) -> Count: ...
|
161
|
-
@abc.abstractmethod
|
162
|
-
def visit_where(self, where: Where, **kw) -> Where: ...
|
163
|
-
@abc.abstractmethod
|
164
|
-
def visit_having(self, having: Having, **kw) -> Having: ...
|
165
|
-
@abc.abstractmethod
|
166
|
-
def visit_order(self, order: Order, **kw) -> Order: ...
|
167
|
-
@abc.abstractmethod
|
168
|
-
def visit_concat(self, concat: Concat, **kw) -> Concat: ...
|
169
|
-
@abc.abstractmethod
|
170
|
-
def visit_max(self, max: Max, **kw) -> Max: ...
|
171
|
-
@abc.abstractmethod
|
172
|
-
def visit_min(self, min: Min, **kw) -> Min: ...
|
173
|
-
@abc.abstractmethod
|
174
|
-
def visit_sum(self, sum: Sum, **kw) -> Sum: ...
|
175
|
-
@abc.abstractmethod
|
176
|
-
def visit_group_by(self, groupby: Groupby, **kw) -> Groupby: ...
|
177
|
-
|
178
144
|
|
179
145
|
class DDLCompiler(Compiled):
|
180
146
|
is_ddl = True
|
@@ -214,8 +180,6 @@ class DDLCompiler(Compiled):
|
|
214
180
|
"""
|
215
181
|
schema_name = create.schema
|
216
182
|
|
217
|
-
util.avoid_sql_injection(schema_name)
|
218
|
-
|
219
183
|
if_not_exists_clause = "IF NOT EXISTS " if create.if_not_exists else ""
|
220
184
|
return f"CREATE SCHEMA {if_not_exists_clause}{schema_name};"
|
221
185
|
|
@@ -239,16 +203,23 @@ class DDLCompiler(Compiled):
|
|
239
203
|
except Exception:
|
240
204
|
raise
|
241
205
|
|
242
|
-
foreign_keys =
|
206
|
+
foreign_keys = create.element.foreign_keys()
|
207
|
+
|
208
|
+
foreign_keys_string = []
|
209
|
+
for fk in foreign_keys.values():
|
210
|
+
foreign_keys_string.append(fk.compile(self.dialect, orig_table=tablecls).string)
|
243
211
|
table_options = " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
244
212
|
|
245
213
|
sql = f"CREATE TABLE {tablecls.__table_name__} (\n\t"
|
246
214
|
sql += ",\n\t".join(column_sql)
|
247
215
|
sql += "\n\t" if not foreign_keys else ",\n\t"
|
248
|
-
sql += ",\n\t".join(
|
216
|
+
sql += ",\n\t".join(foreign_keys_string)
|
249
217
|
sql += f"\n){table_options};"
|
250
218
|
return sql
|
251
219
|
|
220
|
+
def visit_drop_table(self, drop: DropTable, **kw) -> str:
|
221
|
+
return "DROP TABLE " + drop.element.__table_name__
|
222
|
+
|
252
223
|
def visit_create_column(self, create: CreateColumn, first_pk=False, **kw): # noqa: F821
|
253
224
|
column = create.element
|
254
225
|
return self.get_column_specification(column)
|
@@ -273,6 +244,225 @@ class DDLCompiler(Compiled):
|
|
273
244
|
return None
|
274
245
|
return None
|
275
246
|
|
247
|
+
# TODOH []: refactor in order to improve clarity
|
248
|
+
def visit_create_backup(
|
249
|
+
self,
|
250
|
+
backup: CreateBackup,
|
251
|
+
output: Optional[Union[Path | str, BinaryIO, TextIO]] = None,
|
252
|
+
compress: bool = False,
|
253
|
+
backup_dir: customString = ".",
|
254
|
+
**kw,
|
255
|
+
) -> Optional[str | BinaryIO | Path]:
|
256
|
+
"""
|
257
|
+
Create MySQL backup with flexible output options
|
258
|
+
|
259
|
+
Args:
|
260
|
+
backup: An object containing database connection details (host, user, password, database, port).
|
261
|
+
output: Output destination:
|
262
|
+
- None: Auto-generate file path
|
263
|
+
- str: Custom file path (treated as a path-like object)
|
264
|
+
- Stream object: Write to stream (io.StringIO, io.BytesIO, sys.stdout, etc.)
|
265
|
+
compress: Whether to compress the output using gzip.
|
266
|
+
backup_dir: Directory for auto-generated files if 'output' is None.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
- File path (str) if output to file.
|
270
|
+
- Backup data as bytes (if output to binary stream) or string (if output to text stream).
|
271
|
+
- None if an error occurs.
|
272
|
+
"""
|
273
|
+
|
274
|
+
host = backup.url.host
|
275
|
+
user = backup.url.username
|
276
|
+
password = backup.url.password
|
277
|
+
database = backup.url.database
|
278
|
+
port = backup.url.port
|
279
|
+
|
280
|
+
if not database:
|
281
|
+
log.error("Error: Database name is required for backup.")
|
282
|
+
return None
|
283
|
+
|
284
|
+
# Build mysqldump command
|
285
|
+
command = [
|
286
|
+
"mysqldump",
|
287
|
+
f"--host={host}",
|
288
|
+
f"--port={port}",
|
289
|
+
f"--user={user}",
|
290
|
+
f"--password={password}",
|
291
|
+
"--single-transaction",
|
292
|
+
"--routines",
|
293
|
+
"--triggers",
|
294
|
+
"--events",
|
295
|
+
"--lock-tables=false", # Often used to avoid locking during backup
|
296
|
+
"--add-drop-table",
|
297
|
+
"--extended-insert",
|
298
|
+
database,
|
299
|
+
]
|
300
|
+
|
301
|
+
def export_to_stream_internal() -> Optional[io.BytesIO]:
|
302
|
+
nonlocal command, compress, database
|
303
|
+
# If streaming, execute mysqldump and capture stdout
|
304
|
+
log.info(f"Backing up database '{database}' to BytesIO...")
|
305
|
+
|
306
|
+
try:
|
307
|
+
if compress:
|
308
|
+
# Start mysqldump process
|
309
|
+
mysqldump_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
310
|
+
|
311
|
+
# Start gzip process, taking input from mysqldump
|
312
|
+
gzip_process = subprocess.Popen(["gzip", "-c"], stdin=mysqldump_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
313
|
+
|
314
|
+
# Close mysqldump stdout in parent process - gzip will handle it
|
315
|
+
mysqldump_process.stdout.close()
|
316
|
+
|
317
|
+
# Wait for gzip to complete (which will also wait for mysqldump)
|
318
|
+
gzip_stdout, gzip_stderr = gzip_process.communicate()
|
319
|
+
|
320
|
+
# Wait for mysqldump to finish and get its stderr
|
321
|
+
mysqldump_stderr = mysqldump_process.communicate()[1]
|
322
|
+
|
323
|
+
if mysqldump_process.returncode != 0:
|
324
|
+
log.error(f"mysqldump error: {mysqldump_stderr.decode().strip()}")
|
325
|
+
return None
|
326
|
+
if gzip_process.returncode != 0:
|
327
|
+
log.error(f"gzip error: {gzip_stderr.decode().strip()}")
|
328
|
+
return None
|
329
|
+
|
330
|
+
log.info("Backup successful and compressed to BytesIO.")
|
331
|
+
return io.BytesIO(gzip_stdout)
|
332
|
+
else:
|
333
|
+
# Directly capture mysqldump output
|
334
|
+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
335
|
+
stdout, stderr = process.communicate()
|
336
|
+
|
337
|
+
if process.returncode != 0:
|
338
|
+
log.error(f"mysqldump error: {stderr.decode().strip()}")
|
339
|
+
return None
|
340
|
+
|
341
|
+
log.info("Backup successful to BytesIO.")
|
342
|
+
return io.BytesIO(stdout)
|
343
|
+
|
344
|
+
except FileNotFoundError as e:
|
345
|
+
log.error(f"Error: '{e.filename}' command not found. Please ensure mysqldump (and gzip if compressing) is installed and in your system's PATH.")
|
346
|
+
return None
|
347
|
+
except Exception as e:
|
348
|
+
log.error(f"An unexpected error occurred during streaming backup: {e}")
|
349
|
+
return None
|
350
|
+
|
351
|
+
def export_to_file_internal(file_path: customString) -> Optional[Path]:
|
352
|
+
nonlocal command, compress, database
|
353
|
+
|
354
|
+
if isinstance(file_path, str):
|
355
|
+
file_path = Path(file_path)
|
356
|
+
|
357
|
+
if not file_path.exists():
|
358
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
359
|
+
file_path.touch()
|
360
|
+
|
361
|
+
try:
|
362
|
+
if compress:
|
363
|
+
# Pipe mysqldump output through gzip to file
|
364
|
+
with open(file_path, "wb") as output_file:
|
365
|
+
# Start mysqldump process
|
366
|
+
mysqldump_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
367
|
+
|
368
|
+
# Start gzip process, taking input from mysqldump and writing to file
|
369
|
+
gzip_process = subprocess.Popen(["gzip", "-c"], stdin=mysqldump_process.stdout, stdout=output_file, stderr=subprocess.PIPE)
|
370
|
+
|
371
|
+
# Close mysqldump stdout in parent process - gzip will handle it
|
372
|
+
mysqldump_process.stdout.close()
|
373
|
+
|
374
|
+
# Wait for gzip to complete (which will also wait for mysqldump)
|
375
|
+
gzip_stdout, gzip_stderr = gzip_process.communicate()
|
376
|
+
|
377
|
+
# Wait for mysqldump to finish and get its stderr
|
378
|
+
mysqldump_stderr = mysqldump_process.communicate()[1]
|
379
|
+
|
380
|
+
if mysqldump_process.returncode != 0:
|
381
|
+
log.error(f"mysqldump error: {mysqldump_stderr.decode().strip()}")
|
382
|
+
return None
|
383
|
+
if gzip_process.returncode != 0:
|
384
|
+
log.error(f"gzip error: {gzip_stderr.decode().strip()}")
|
385
|
+
return None
|
386
|
+
else:
|
387
|
+
# Directly redirect mysqldump output to file
|
388
|
+
with open(file_path, "wb") as output_file:
|
389
|
+
process = subprocess.Popen(command, stdout=output_file, stderr=subprocess.PIPE)
|
390
|
+
stdout, stderr = process.communicate()
|
391
|
+
|
392
|
+
if process.returncode != 0:
|
393
|
+
log.error(f"mysqldump error: {stderr.decode().strip()}")
|
394
|
+
return None
|
395
|
+
|
396
|
+
log.info(f"Backup completed successfully: {file_path}")
|
397
|
+
return file_path
|
398
|
+
|
399
|
+
except FileNotFoundError as e:
|
400
|
+
log.error(f"Error: '{e.filename}' command not found. Please ensure mysqldump (and gzip if compressing) is installed and in your system's PATH.")
|
401
|
+
return None
|
402
|
+
except Exception as e:
|
403
|
+
log.error(f"An unexpected error occurred during file backup: {e}")
|
404
|
+
return None
|
405
|
+
|
406
|
+
try:
|
407
|
+
if output is None:
|
408
|
+
# Auto-generate file path
|
409
|
+
|
410
|
+
backup_dir = Path(backup_dir)
|
411
|
+
backup_dir.mkdir(exist_ok=True)
|
412
|
+
|
413
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
414
|
+
file_extension = "sql.gz" if compress else "sql"
|
415
|
+
output_filename = f"{database}_backup_{timestamp}.{file_extension}"
|
416
|
+
output_filepath = os.path.join(backup_dir, output_filename)
|
417
|
+
return export_to_file_internal(output_filepath)
|
418
|
+
|
419
|
+
elif isinstance(output, (io.BytesIO, io.StringIO)):
|
420
|
+
# Output to a stream object
|
421
|
+
stream_result = export_to_stream_internal()
|
422
|
+
if stream_result:
|
423
|
+
# Write the content from the internal BytesIO to the provided output stream
|
424
|
+
if isinstance(output, io.BytesIO):
|
425
|
+
output.write(stream_result.getvalue())
|
426
|
+
return stream_result.getvalue() # Return bytes if it was a BytesIO internally
|
427
|
+
elif isinstance(output, io.StringIO):
|
428
|
+
# Attempt to decode bytes to string if target is StringIO
|
429
|
+
try:
|
430
|
+
decoded_content = stream_result.getvalue().decode("utf-8")
|
431
|
+
output.write(decoded_content)
|
432
|
+
return decoded_content
|
433
|
+
except UnicodeDecodeError:
|
434
|
+
log.error("Error: Cannot decode byte stream to UTF-8 for StringIO output. Consider setting compress=False or ensuring database encoding is compatible.")
|
435
|
+
return None
|
436
|
+
return None
|
437
|
+
|
438
|
+
elif isinstance(output, str | Path):
|
439
|
+
# Output to a custom file path
|
440
|
+
return export_to_file_internal(output)
|
441
|
+
|
442
|
+
elif isinstance(output, (BinaryIO, TextIO)): # Handles sys.stdout, open file objects
|
443
|
+
stream_result = export_to_stream_internal()
|
444
|
+
if stream_result:
|
445
|
+
if "b" in getattr(output, "mode", "") or isinstance(output, BinaryIO): # Check if it's a binary stream
|
446
|
+
output.write(stream_result.getvalue())
|
447
|
+
return stream_result.getvalue()
|
448
|
+
else: # Assume text stream
|
449
|
+
try:
|
450
|
+
decoded_content = stream_result.getvalue().decode("utf-8")
|
451
|
+
output.write(decoded_content)
|
452
|
+
return decoded_content
|
453
|
+
except UnicodeDecodeError:
|
454
|
+
log.error("Error: Cannot decode byte stream to UTF-8 for text stream output. Consider setting compress=False or ensuring database encoding is compatible.")
|
455
|
+
return None
|
456
|
+
return None
|
457
|
+
|
458
|
+
else:
|
459
|
+
log.error(f"Unsupported output type: {type(output)}")
|
460
|
+
return None
|
461
|
+
|
462
|
+
except Exception as e:
|
463
|
+
log.error(f"An unexpected error occurred: {e}")
|
464
|
+
return None
|
465
|
+
|
276
466
|
|
277
467
|
class GenericTypeCompiler(TypeCompiler):
|
278
468
|
"""Generic type compiler
|