ormlambda 3.35.2__py3-none-any.whl → 4.0.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.
Files changed (130) hide show
  1. ormlambda/__init__.py +79 -51
  2. ormlambda/caster/caster.py +6 -1
  3. ormlambda/common/abstract_classes/__init__.py +0 -2
  4. ormlambda/common/enums/__init__.py +1 -0
  5. ormlambda/common/enums/order_type.py +9 -0
  6. ormlambda/common/errors/__init__.py +13 -3
  7. ormlambda/common/global_checker.py +86 -8
  8. ormlambda/common/interfaces/IQueryCommand.py +2 -2
  9. ormlambda/common/interfaces/__init__.py +0 -2
  10. ormlambda/dialects/__init__.py +75 -3
  11. ormlambda/dialects/default/base.py +1 -1
  12. ormlambda/dialects/mysql/__init__.py +35 -78
  13. ormlambda/dialects/mysql/base.py +226 -40
  14. ormlambda/dialects/mysql/clauses/ST_AsText.py +26 -0
  15. ormlambda/dialects/mysql/clauses/ST_Contains.py +30 -0
  16. ormlambda/dialects/mysql/clauses/__init__.py +1 -0
  17. ormlambda/dialects/mysql/repository/__init__.py +1 -0
  18. ormlambda/{databases/my_sql → dialects/mysql/repository}/repository.py +0 -5
  19. ormlambda/dialects/mysql/types.py +6 -0
  20. ormlambda/engine/base.py +26 -4
  21. ormlambda/errors.py +9 -0
  22. ormlambda/model/base_model.py +3 -10
  23. ormlambda/repository/base_repository.py +1 -1
  24. ormlambda/repository/interfaces/IRepositoryBase.py +0 -7
  25. ormlambda/repository/response.py +12 -7
  26. ormlambda/sql/__init__.py +12 -3
  27. ormlambda/sql/clause_info/__init__.py +0 -2
  28. ormlambda/sql/clause_info/clause_info.py +94 -76
  29. ormlambda/sql/clause_info/interface/IAggregate.py +14 -4
  30. ormlambda/sql/clause_info/interface/IClauseInfo.py +6 -11
  31. ormlambda/sql/clauses/alias.py +6 -37
  32. ormlambda/sql/clauses/count.py +21 -36
  33. ormlambda/sql/clauses/group_by.py +13 -19
  34. ormlambda/sql/clauses/having.py +2 -6
  35. ormlambda/sql/clauses/insert.py +3 -3
  36. ormlambda/sql/clauses/interfaces/__init__.py +0 -1
  37. ormlambda/sql/clauses/join/join_context.py +5 -12
  38. ormlambda/sql/clauses/joins.py +34 -52
  39. ormlambda/sql/clauses/limit.py +1 -2
  40. ormlambda/sql/clauses/offset.py +1 -2
  41. ormlambda/sql/clauses/order.py +17 -21
  42. ormlambda/sql/clauses/select.py +56 -28
  43. ormlambda/sql/clauses/update.py +13 -10
  44. ormlambda/sql/clauses/where.py +20 -39
  45. ormlambda/sql/column/__init__.py +1 -0
  46. ormlambda/sql/column/column.py +19 -12
  47. ormlambda/sql/column/column_proxy.py +117 -0
  48. ormlambda/sql/column_table_proxy.py +23 -0
  49. ormlambda/sql/comparer.py +31 -65
  50. ormlambda/sql/compiler.py +248 -58
  51. ormlambda/sql/context/__init__.py +304 -0
  52. ormlambda/sql/ddl.py +19 -5
  53. ormlambda/sql/elements.py +3 -0
  54. ormlambda/sql/foreign_key.py +42 -64
  55. ormlambda/sql/functions/__init__.py +0 -1
  56. ormlambda/sql/functions/concat.py +35 -38
  57. ormlambda/sql/functions/max.py +12 -36
  58. ormlambda/sql/functions/min.py +13 -28
  59. ormlambda/sql/functions/sum.py +17 -33
  60. ormlambda/sql/sqltypes.py +2 -0
  61. ormlambda/sql/table/__init__.py +1 -0
  62. ormlambda/sql/table/table.py +32 -49
  63. ormlambda/sql/table/table_proxy.py +88 -0
  64. ormlambda/sql/type_api.py +4 -1
  65. ormlambda/sql/types.py +15 -12
  66. ormlambda/statements/__init__.py +0 -2
  67. ormlambda/statements/base_statement.py +51 -84
  68. ormlambda/statements/interfaces/IStatements.py +77 -123
  69. ormlambda/statements/interfaces/__init__.py +1 -1
  70. ormlambda/statements/query_builder.py +296 -128
  71. ormlambda/statements/statements.py +120 -110
  72. ormlambda/statements/types.py +5 -25
  73. ormlambda/util/__init__.py +7 -86
  74. ormlambda/util/langhelpers.py +102 -0
  75. ormlambda/util/module_tree/dynamic_module.py +1 -1
  76. ormlambda/util/preloaded.py +80 -0
  77. ormlambda/util/typing.py +12 -3
  78. {ormlambda-3.35.2.dist-info → ormlambda-4.0.0.dist-info}/METADATA +29 -31
  79. ormlambda-4.0.0.dist-info/RECORD +139 -0
  80. ormlambda/common/abstract_classes/clause_info_converter.py +0 -65
  81. ormlambda/common/abstract_classes/decomposition_query.py +0 -141
  82. ormlambda/common/abstract_classes/query_base.py +0 -15
  83. ormlambda/common/interfaces/ICustomAlias.py +0 -7
  84. ormlambda/common/interfaces/IDecompositionQuery.py +0 -33
  85. ormlambda/databases/__init__.py +0 -4
  86. ormlambda/databases/my_sql/__init__.py +0 -3
  87. ormlambda/databases/my_sql/clauses/ST_AsText.py +0 -37
  88. ormlambda/databases/my_sql/clauses/ST_Contains.py +0 -36
  89. ormlambda/databases/my_sql/clauses/__init__.py +0 -14
  90. ormlambda/databases/my_sql/clauses/count.py +0 -33
  91. ormlambda/databases/my_sql/clauses/delete.py +0 -9
  92. ormlambda/databases/my_sql/clauses/drop_table.py +0 -26
  93. ormlambda/databases/my_sql/clauses/group_by.py +0 -17
  94. ormlambda/databases/my_sql/clauses/having.py +0 -12
  95. ormlambda/databases/my_sql/clauses/insert.py +0 -9
  96. ormlambda/databases/my_sql/clauses/joins.py +0 -14
  97. ormlambda/databases/my_sql/clauses/limit.py +0 -6
  98. ormlambda/databases/my_sql/clauses/offset.py +0 -6
  99. ormlambda/databases/my_sql/clauses/order.py +0 -8
  100. ormlambda/databases/my_sql/clauses/update.py +0 -8
  101. ormlambda/databases/my_sql/clauses/upsert.py +0 -9
  102. ormlambda/databases/my_sql/clauses/where.py +0 -7
  103. ormlambda/dialects/interface/__init__.py +0 -1
  104. ormlambda/dialects/interface/dialect.py +0 -78
  105. ormlambda/sql/clause_info/aggregate_function_base.py +0 -96
  106. ormlambda/sql/clause_info/clause_info_context.py +0 -87
  107. ormlambda/sql/clauses/interfaces/ISelect.py +0 -17
  108. ormlambda/sql/clauses/new_join.py +0 -119
  109. ormlambda/util/load_module.py +0 -21
  110. ormlambda/util/plugin_loader.py +0 -32
  111. ormlambda-3.35.2.dist-info/RECORD +0 -159
  112. /ormlambda/{databases/my_sql → dialects/mysql}/caster/__init__.py +0 -0
  113. /ormlambda/{databases/my_sql → dialects/mysql}/caster/caster.py +0 -0
  114. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/__init__.py +0 -0
  115. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/boolean.py +0 -0
  116. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/bytes.py +0 -0
  117. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/date.py +0 -0
  118. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/datetime.py +0 -0
  119. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/decimal.py +0 -0
  120. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/float.py +0 -0
  121. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/int.py +0 -0
  122. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/iterable.py +0 -0
  123. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/json.py +0 -0
  124. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/none.py +0 -0
  125. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/point.py +0 -0
  126. /ormlambda/{databases/my_sql → dialects/mysql}/caster/types/string.py +0 -0
  127. /ormlambda/{databases/my_sql → dialects/mysql/repository}/pool_types.py +0 -0
  128. {ormlambda-3.35.2.dist-info → ormlambda-4.0.0.dist-info}/AUTHORS +0 -0
  129. {ormlambda-3.35.2.dist-info → ormlambda-4.0.0.dist-info}/LICENSE +0 -0
  130. {ormlambda-3.35.2.dist-info → ormlambda-4.0.0.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, ComparerTypes
9
- from ormlambda.sql.clause_info import ClauseInfo
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 Element
12
+ from ormlambda.sql.elements import ClauseElement
12
13
 
13
- if tp.TYPE_CHECKING:
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(Element, IQuery):
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._left_condition: Comparer | ClauseInfo = left_condition
64
- self._right_condition: Comparer | ClauseInfo = right_condition
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
- def set_dialect(self, dialect: Dialect) -> None:
73
- self._dialect = dialect
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.query}"
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, context: ClauseContextType = None, *, dialect) -> str:
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._context = context
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
- right_comparer._context = context
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
- return new_comparer.query(dialect)
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, context)
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 abc
3
- from typing import Any, ClassVar, Optional, TYPE_CHECKING
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 CreateTable, CreateSchema, DropSchema
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
- from ormlambda.sql.clauses import (
47
- Insert,
48
- Delete,
49
- Upsert,
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, abc.ABC):
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 = ForeignKey.create_query(tablecls, self.dialect)
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(foreign_keys)
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