ormlambda 3.12.2__py3-none-any.whl → 3.34.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.
- ormlambda/__init__.py +2 -0
- ormlambda/caster/__init__.py +1 -1
- ormlambda/caster/caster.py +29 -12
- ormlambda/common/abstract_classes/clause_info_converter.py +4 -12
- ormlambda/common/abstract_classes/decomposition_query.py +17 -2
- ormlambda/common/abstract_classes/non_query_base.py +9 -7
- ormlambda/common/abstract_classes/query_base.py +3 -1
- ormlambda/common/errors/__init__.py +29 -0
- ormlambda/common/interfaces/IQueryCommand.py +6 -2
- ormlambda/dialects/__init__.py +39 -0
- ormlambda/dialects/default/__init__.py +1 -0
- ormlambda/dialects/default/base.py +39 -0
- ormlambda/dialects/interface/__init__.py +1 -0
- ormlambda/dialects/interface/dialect.py +78 -0
- ormlambda/dialects/mysql/__init__.py +38 -0
- ormlambda/dialects/mysql/base.py +388 -0
- ormlambda/dialects/mysql/caster/caster.py +39 -0
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/__init__.py +1 -0
- ormlambda/dialects/mysql/caster/types/boolean.py +35 -0
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/bytes.py +7 -7
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/datetime.py +7 -7
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/float.py +7 -7
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/int.py +7 -7
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/iterable.py +7 -7
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/none.py +8 -7
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/point.py +4 -4
- ormlambda/{databases/my_sql → dialects/mysql}/caster/types/string.py +7 -7
- ormlambda/{databases/my_sql → dialects/mysql}/clauses/ST_AsText.py +8 -7
- ormlambda/{databases/my_sql → dialects/mysql}/clauses/ST_Contains.py +10 -5
- ormlambda/dialects/mysql/clauses/__init__.py +13 -0
- ormlambda/dialects/mysql/clauses/count.py +33 -0
- ormlambda/dialects/mysql/clauses/delete.py +9 -0
- ormlambda/dialects/mysql/clauses/group_by.py +17 -0
- ormlambda/dialects/mysql/clauses/having.py +12 -0
- ormlambda/dialects/mysql/clauses/insert.py +9 -0
- ormlambda/dialects/mysql/clauses/joins.py +14 -0
- ormlambda/dialects/mysql/clauses/limit.py +6 -0
- ormlambda/dialects/mysql/clauses/offset.py +6 -0
- ormlambda/dialects/mysql/clauses/order.py +8 -0
- ormlambda/dialects/mysql/clauses/update.py +8 -0
- ormlambda/dialects/mysql/clauses/upsert.py +9 -0
- ormlambda/dialects/mysql/clauses/where.py +7 -0
- ormlambda/dialects/mysql/mysqlconnector.py +46 -0
- ormlambda/dialects/mysql/repository/__init__.py +1 -0
- ormlambda/dialects/mysql/repository/repository.py +212 -0
- ormlambda/dialects/mysql/types.py +732 -0
- ormlambda/dialects/sqlite/__init__.py +5 -0
- ormlambda/dialects/sqlite/base.py +47 -0
- ormlambda/dialects/sqlite/pysqlite.py +32 -0
- ormlambda/engine/__init__.py +1 -0
- ormlambda/engine/base.py +77 -0
- ormlambda/engine/create.py +9 -23
- ormlambda/engine/url.py +31 -19
- ormlambda/env.py +30 -0
- ormlambda/errors.py +17 -0
- ormlambda/model/base_model.py +7 -9
- ormlambda/repository/base_repository.py +36 -5
- ormlambda/repository/interfaces/IRepositoryBase.py +119 -12
- ormlambda/repository/response.py +134 -0
- ormlambda/sql/clause_info/aggregate_function_base.py +19 -9
- ormlambda/sql/clause_info/clause_info.py +34 -17
- ormlambda/sql/clauses/__init__.py +14 -0
- ormlambda/{databases/my_sql → sql}/clauses/alias.py +23 -6
- ormlambda/{databases/my_sql → sql}/clauses/count.py +15 -1
- ormlambda/{databases/my_sql → sql}/clauses/delete.py +22 -7
- ormlambda/sql/clauses/group_by.py +30 -0
- ormlambda/{databases/my_sql → sql}/clauses/having.py +7 -2
- ormlambda/{databases/my_sql → sql}/clauses/insert.py +16 -9
- ormlambda/sql/clauses/interfaces/__init__.py +5 -0
- ormlambda/{components → sql/clauses}/join/join_context.py +15 -7
- ormlambda/{databases/my_sql → sql}/clauses/joins.py +29 -19
- ormlambda/sql/clauses/limit.py +15 -0
- ormlambda/sql/clauses/offset.py +15 -0
- ormlambda/{databases/my_sql → sql}/clauses/order.py +14 -24
- ormlambda/{databases/my_sql → sql}/clauses/select.py +12 -13
- ormlambda/{databases/my_sql → sql}/clauses/update.py +24 -11
- ormlambda/{databases/my_sql → sql}/clauses/upsert.py +17 -12
- ormlambda/{databases/my_sql → sql}/clauses/where.py +28 -8
- ormlambda/sql/column/__init__.py +1 -0
- ormlambda/sql/{column.py → column/column.py} +82 -22
- ormlambda/sql/comparer.py +51 -37
- ormlambda/sql/compiler.py +668 -0
- ormlambda/sql/ddl.py +82 -0
- ormlambda/sql/elements.py +36 -0
- ormlambda/sql/foreign_key.py +61 -39
- ormlambda/{databases/my_sql → sql}/functions/concat.py +13 -5
- ormlambda/{databases/my_sql → sql}/functions/max.py +9 -4
- ormlambda/{databases/my_sql → sql}/functions/min.py +9 -13
- ormlambda/{databases/my_sql → sql}/functions/sum.py +8 -10
- ormlambda/sql/sqltypes.py +647 -0
- ormlambda/sql/table/__init__.py +1 -1
- ormlambda/sql/table/table.py +175 -0
- ormlambda/sql/table/table_constructor.py +1 -208
- ormlambda/sql/type_api.py +35 -0
- ormlambda/sql/types.py +3 -1
- ormlambda/sql/visitors.py +74 -0
- ormlambda/statements/__init__.py +1 -0
- ormlambda/statements/base_statement.py +28 -38
- ormlambda/statements/interfaces/IStatements.py +8 -4
- ormlambda/{databases/my_sql → statements}/query_builder.py +35 -30
- ormlambda/{databases/my_sql → statements}/statements.py +57 -61
- ormlambda/statements/types.py +2 -2
- ormlambda/types/__init__.py +24 -0
- ormlambda/types/metadata.py +42 -0
- ormlambda/util/__init__.py +87 -0
- ormlambda/{utils → util}/module_tree/dynamic_module.py +1 -1
- ormlambda/util/plugin_loader.py +32 -0
- ormlambda/util/typing.py +6 -0
- ormlambda-3.34.0.dist-info/AUTHORS +32 -0
- {ormlambda-3.12.2.dist-info → ormlambda-3.34.0.dist-info}/METADATA +1 -1
- ormlambda-3.34.0.dist-info/RECORD +152 -0
- ormlambda/components/__init__.py +0 -4
- ormlambda/components/delete/__init__.py +0 -2
- ormlambda/components/delete/abstract_delete.py +0 -17
- ormlambda/components/insert/__init__.py +0 -2
- ormlambda/components/insert/abstract_insert.py +0 -25
- ormlambda/components/select/__init__.py +0 -1
- ormlambda/components/update/__init__.py +0 -2
- ormlambda/components/update/abstract_update.py +0 -29
- ormlambda/components/upsert/__init__.py +0 -2
- ormlambda/components/upsert/abstract_upsert.py +0 -25
- ormlambda/databases/__init__.py +0 -5
- ormlambda/databases/my_sql/__init__.py +0 -4
- ormlambda/databases/my_sql/caster/caster.py +0 -39
- ormlambda/databases/my_sql/clauses/__init__.py +0 -20
- ormlambda/databases/my_sql/clauses/create_database.py +0 -35
- ormlambda/databases/my_sql/clauses/drop_database.py +0 -17
- ormlambda/databases/my_sql/clauses/drop_table.py +0 -26
- ormlambda/databases/my_sql/clauses/group_by.py +0 -30
- ormlambda/databases/my_sql/clauses/limit.py +0 -17
- ormlambda/databases/my_sql/clauses/offset.py +0 -17
- ormlambda/databases/my_sql/repository/__init__.py +0 -1
- ormlambda/databases/my_sql/repository/repository.py +0 -351
- ormlambda/engine/template.py +0 -47
- ormlambda/sql/dtypes.py +0 -94
- ormlambda/utils/__init__.py +0 -1
- ormlambda-3.12.2.dist-info/RECORD +0 -125
- /ormlambda/{databases/my_sql → dialects/mysql}/caster/__init__.py +0 -0
- /ormlambda/{databases/my_sql/types.py → dialects/mysql/repository/pool_types.py} +0 -0
- /ormlambda/{components/delete → sql/clauses/interfaces}/IDelete.py +0 -0
- /ormlambda/{components/insert → sql/clauses/interfaces}/IInsert.py +0 -0
- /ormlambda/{components/select → sql/clauses/interfaces}/ISelect.py +0 -0
- /ormlambda/{components/update → sql/clauses/interfaces}/IUpdate.py +0 -0
- /ormlambda/{components/upsert → sql/clauses/interfaces}/IUpsert.py +0 -0
- /ormlambda/{components → sql/clauses}/join/__init__.py +0 -0
- /ormlambda/{databases/my_sql → sql}/functions/__init__.py +0 -0
- /ormlambda/{utils → util}/module_tree/__init__.py +0 -0
- /ormlambda/{utils → util}/module_tree/dfs_traversal.py +0 -0
- {ormlambda-3.12.2.dist-info → ormlambda-3.34.0.dist-info}/LICENSE +0 -0
- {ormlambda-3.12.2.dist-info → ormlambda-3.34.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,668 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import abc
|
3
|
+
import datetime
|
4
|
+
import io
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
from pathlib import Path
|
8
|
+
import subprocess
|
9
|
+
import sys
|
10
|
+
from typing import Any, BinaryIO, ClassVar, Optional, TYPE_CHECKING, TextIO, Union
|
11
|
+
|
12
|
+
from ormlambda.sql.ddl import CreateColumn
|
13
|
+
from ormlambda.sql.foreign_key import ForeignKey
|
14
|
+
from ormlambda.sql.sqltypes import resolve_primitive_types
|
15
|
+
|
16
|
+
from .visitors import Visitor
|
17
|
+
from ormlambda import util
|
18
|
+
from ormlambda.sql.type_api import TypeEngine
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from ormlambda import Column
|
22
|
+
from .visitors import Element
|
23
|
+
from .elements import ClauseElement
|
24
|
+
from ormlambda.dialects import Dialect
|
25
|
+
from ormlambda.sql.ddl import (
|
26
|
+
CreateTable,
|
27
|
+
CreateSchema,
|
28
|
+
DropSchema,
|
29
|
+
DropTable,
|
30
|
+
CreateBackup,
|
31
|
+
)
|
32
|
+
from .sqltypes import (
|
33
|
+
INTEGER,
|
34
|
+
SMALLINTEGER,
|
35
|
+
BIGINTEGER,
|
36
|
+
NUMERIC,
|
37
|
+
FLOAT,
|
38
|
+
REAL,
|
39
|
+
DOUBLE,
|
40
|
+
STRING,
|
41
|
+
TEXT,
|
42
|
+
UNICODE,
|
43
|
+
UNICODETEXT,
|
44
|
+
NCHAR,
|
45
|
+
VARCHAR,
|
46
|
+
NVARCHAR,
|
47
|
+
CHAR,
|
48
|
+
DATE,
|
49
|
+
TIME,
|
50
|
+
DATETIME,
|
51
|
+
TIMESTAMP,
|
52
|
+
BOOLEAN,
|
53
|
+
LARGEBINARY,
|
54
|
+
VARBINARY,
|
55
|
+
ENUM,
|
56
|
+
POINT,
|
57
|
+
)
|
58
|
+
|
59
|
+
from ormlambda.sql.clauses import (
|
60
|
+
Insert,
|
61
|
+
Delete,
|
62
|
+
Upsert,
|
63
|
+
Update,
|
64
|
+
Limit,
|
65
|
+
Offset,
|
66
|
+
Count,
|
67
|
+
Where,
|
68
|
+
Having,
|
69
|
+
Order,
|
70
|
+
Concat,
|
71
|
+
Max,
|
72
|
+
Min,
|
73
|
+
Sum,
|
74
|
+
Groupby,
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
type customString = Union[str | Path]
|
79
|
+
|
80
|
+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
81
|
+
log = logging.getLogger(__name__)
|
82
|
+
|
83
|
+
|
84
|
+
class Compiled:
|
85
|
+
"""Represent a compiled SQL or DDL expression.
|
86
|
+
|
87
|
+
The ``__str__`` method of the ``Compiled`` object should produce
|
88
|
+
the actual text of the statement. ``Compiled`` objects are
|
89
|
+
specific to their underlying database dialect, and also may
|
90
|
+
or may not be specific to the columns referenced within a
|
91
|
+
particular set of bind parameters. In no case should the
|
92
|
+
``Compiled`` object be dependent on the actual values of those
|
93
|
+
bind parameters, even though it may reference those values as
|
94
|
+
defaults.
|
95
|
+
"""
|
96
|
+
|
97
|
+
dialect: Dialect
|
98
|
+
"The dialect to compile against."
|
99
|
+
|
100
|
+
statement: Optional[ClauseElement] = None
|
101
|
+
"The statement to compile."
|
102
|
+
|
103
|
+
string: str = ""
|
104
|
+
"The string representation of the ``statement``"
|
105
|
+
|
106
|
+
_gen_time: float
|
107
|
+
"The time when the statement was generated."
|
108
|
+
|
109
|
+
is_sql: ClassVar[bool] = False
|
110
|
+
is_ddl: ClassVar[bool] = False
|
111
|
+
|
112
|
+
def __init__(
|
113
|
+
self,
|
114
|
+
dialect: Dialect,
|
115
|
+
statement: Optional[ClauseElement] = None,
|
116
|
+
**kw: Any,
|
117
|
+
) -> None:
|
118
|
+
"""Construct a new :class:`.Compiled` object.
|
119
|
+
|
120
|
+
:param dialect: :class:`.Dialect` to compile against.
|
121
|
+
|
122
|
+
:param statement: :class:`_expression.ClauseElement` to be compiled.
|
123
|
+
|
124
|
+
"""
|
125
|
+
self.dialect = dialect
|
126
|
+
|
127
|
+
if statement is not None:
|
128
|
+
self.statement = statement
|
129
|
+
self.string = self.process(self.statement, **kw)
|
130
|
+
|
131
|
+
@property
|
132
|
+
def sql_compiler(self):
|
133
|
+
"""Return a Compiled that is capable of processing SQL expressions.
|
134
|
+
|
135
|
+
If this compiler is one, it would likely just return 'self'.
|
136
|
+
|
137
|
+
"""
|
138
|
+
|
139
|
+
raise NotImplementedError()
|
140
|
+
|
141
|
+
def process(self, obj: Element, **kwargs: Any) -> str:
|
142
|
+
return obj._compiler_dispatch(self, **kwargs)
|
143
|
+
|
144
|
+
|
145
|
+
class TypeCompiler(Visitor):
|
146
|
+
"""Base class for all type compilers."""
|
147
|
+
|
148
|
+
def __init__(self, dialect: Dialect):
|
149
|
+
self.dialect = dialect
|
150
|
+
|
151
|
+
def process(self, type_: TypeEngine[Any], **kw: Any) -> str:
|
152
|
+
"""Process a type object into a string representation.
|
153
|
+
|
154
|
+
:param type_: The type object to process.
|
155
|
+
:param kw: Additional keyword arguments.
|
156
|
+
:return: The string representation of the type object.
|
157
|
+
"""
|
158
|
+
if not isinstance(type_, TypeEngine):
|
159
|
+
type_ = resolve_primitive_types(type_)
|
160
|
+
return type_._compiler_dispatch(self, **kw)
|
161
|
+
|
162
|
+
|
163
|
+
class SQLCompiler(Compiled, abc.ABC):
|
164
|
+
is_sql = True
|
165
|
+
|
166
|
+
@abc.abstractmethod
|
167
|
+
def visit_insert(self, insert: Insert, **kw) -> Insert: ...
|
168
|
+
@abc.abstractmethod
|
169
|
+
def visit_delete(self, delete: Delete, **kw) -> Delete: ...
|
170
|
+
@abc.abstractmethod
|
171
|
+
def visit_upsert(self, upsert: Upsert, **kw) -> Upsert: ...
|
172
|
+
@abc.abstractmethod
|
173
|
+
def visit_update(self, update: Update, **kw) -> Update: ...
|
174
|
+
@abc.abstractmethod
|
175
|
+
def visit_limit(self, limit: Limit, **kw) -> Limit: ...
|
176
|
+
@abc.abstractmethod
|
177
|
+
def visit_offset(self, offset: Offset, **kw) -> Offset: ...
|
178
|
+
@abc.abstractmethod
|
179
|
+
def visit_count(self, count: Count, **kw) -> Count: ...
|
180
|
+
@abc.abstractmethod
|
181
|
+
def visit_where(self, where: Where, **kw) -> Where: ...
|
182
|
+
@abc.abstractmethod
|
183
|
+
def visit_having(self, having: Having, **kw) -> Having: ...
|
184
|
+
@abc.abstractmethod
|
185
|
+
def visit_order(self, order: Order, **kw) -> Order: ...
|
186
|
+
@abc.abstractmethod
|
187
|
+
def visit_concat(self, concat: Concat, **kw) -> Concat: ...
|
188
|
+
@abc.abstractmethod
|
189
|
+
def visit_max(self, max: Max, **kw) -> Max: ...
|
190
|
+
@abc.abstractmethod
|
191
|
+
def visit_min(self, min: Min, **kw) -> Min: ...
|
192
|
+
@abc.abstractmethod
|
193
|
+
def visit_sum(self, sum: Sum, **kw) -> Sum: ...
|
194
|
+
@abc.abstractmethod
|
195
|
+
def visit_group_by(self, groupby: Groupby, **kw) -> Groupby: ...
|
196
|
+
|
197
|
+
|
198
|
+
class DDLCompiler(Compiled):
|
199
|
+
is_ddl = True
|
200
|
+
|
201
|
+
if TYPE_CHECKING:
|
202
|
+
|
203
|
+
def __init__(
|
204
|
+
self,
|
205
|
+
dialect: Dialect,
|
206
|
+
statement: Optional[ClauseElement] = None,
|
207
|
+
**kw: Any,
|
208
|
+
) -> None: ...
|
209
|
+
|
210
|
+
@property
|
211
|
+
def sql_compiler(self):
|
212
|
+
"""Return a SQL compiler that is capable of processing SQL expressions.
|
213
|
+
|
214
|
+
This method returns the SQL compiler for the dialect, which is
|
215
|
+
used to process SQL expressions.
|
216
|
+
|
217
|
+
"""
|
218
|
+
return self.dialect.statement_compiler(self.dialect, None)
|
219
|
+
|
220
|
+
def visit_create_schema(self, create: CreateSchema, **kw) -> str:
|
221
|
+
"""
|
222
|
+
Generate a CREATE SCHEMA SQL statement for MySQL.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
schema_name (str): Name of the schema/database to create
|
226
|
+
if_not_exists (bool): Whether to include IF NOT EXISTS clause
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
str: The SQL CREATE SCHEMA statement
|
230
|
+
|
231
|
+
Raises:
|
232
|
+
ValueError: If schema_name is empty or contains invalid characters
|
233
|
+
"""
|
234
|
+
schema_name = create.schema
|
235
|
+
|
236
|
+
util.avoid_sql_injection(schema_name)
|
237
|
+
|
238
|
+
if_not_exists_clause = "IF NOT EXISTS " if create.if_not_exists else ""
|
239
|
+
return f"CREATE SCHEMA {if_not_exists_clause}{schema_name};"
|
240
|
+
|
241
|
+
def visit_drop_schema(self, drop: DropSchema, **kw):
|
242
|
+
if_exists_clause = "IF EXISTS " if drop.if_exists else ""
|
243
|
+
return f"DROP SCHEMA {if_exists_clause}{drop.schema};"
|
244
|
+
|
245
|
+
def visit_schema_exists(self, schema: str) -> bool:
|
246
|
+
return f"SHOW DATABASES LIKE {schema};"
|
247
|
+
# return f"SHOW DATABASES LIKE {self.dialect.caster.PLACEHOLDER};", (schema,)
|
248
|
+
|
249
|
+
def visit_create_table(self, create: CreateTable, **kw) -> str:
|
250
|
+
tablecls = create.element
|
251
|
+
column_sql: list[str] = []
|
252
|
+
for create_col in create.columns:
|
253
|
+
try:
|
254
|
+
processed = self.process(create_col)
|
255
|
+
if processed is not None:
|
256
|
+
column_sql.append(processed)
|
257
|
+
|
258
|
+
except Exception:
|
259
|
+
raise
|
260
|
+
|
261
|
+
foreign_keys = ForeignKey.create_query(tablecls, self.dialect)
|
262
|
+
table_options = " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
263
|
+
|
264
|
+
sql = f"CREATE TABLE {tablecls.__table_name__} (\n\t"
|
265
|
+
sql += ",\n\t".join(column_sql)
|
266
|
+
sql += "\n\t" if not foreign_keys else ",\n\t"
|
267
|
+
sql += ",\n\t".join(foreign_keys)
|
268
|
+
sql += f"\n){table_options};"
|
269
|
+
return sql
|
270
|
+
|
271
|
+
def visit_drop_table(self, drop: DropTable, **kw) -> str:
|
272
|
+
return "DROP TABLE " + drop.element.__table_name__
|
273
|
+
|
274
|
+
def visit_create_column(self, create: CreateColumn, first_pk=False, **kw): # noqa: F821
|
275
|
+
column = create.element
|
276
|
+
return self.get_column_specification(column)
|
277
|
+
|
278
|
+
def get_column_specification(self, column: Column, **kwargs):
|
279
|
+
colspec = column.column_name + " " + self.dialect.type_compiler_instance.process(column.dtype)
|
280
|
+
default = self.get_column_default_string(column)
|
281
|
+
if default is not None:
|
282
|
+
colspec += " DEFAULT " + default
|
283
|
+
|
284
|
+
if column.is_not_null:
|
285
|
+
colspec += " NOT NULL"
|
286
|
+
|
287
|
+
if column.is_primary_key:
|
288
|
+
colspec += " PRIMARY KEY"
|
289
|
+
return colspec
|
290
|
+
|
291
|
+
def get_column_default_string(self, column: Column) -> Optional[str]:
|
292
|
+
if isinstance(column.default_value, str):
|
293
|
+
return column.default_value
|
294
|
+
if not column.default_value:
|
295
|
+
return None
|
296
|
+
return None
|
297
|
+
|
298
|
+
#TODOH []: refactor in order to improve clarity
|
299
|
+
def visit_create_backup(
|
300
|
+
self,
|
301
|
+
backup: CreateBackup,
|
302
|
+
output: Optional[Union[Path | str, BinaryIO, TextIO]] = None,
|
303
|
+
compress: bool = False,
|
304
|
+
backup_dir: customString = ".",
|
305
|
+
**kw,
|
306
|
+
) -> Optional[str | BinaryIO | Path]:
|
307
|
+
"""
|
308
|
+
Create MySQL backup with flexible output options
|
309
|
+
|
310
|
+
Args:
|
311
|
+
backup: An object containing database connection details (host, user, password, database, port).
|
312
|
+
output: Output destination:
|
313
|
+
- None: Auto-generate file path
|
314
|
+
- str: Custom file path (treated as a path-like object)
|
315
|
+
- Stream object: Write to stream (io.StringIO, io.BytesIO, sys.stdout, etc.)
|
316
|
+
compress: Whether to compress the output using gzip.
|
317
|
+
backup_dir: Directory for auto-generated files if 'output' is None.
|
318
|
+
|
319
|
+
Returns:
|
320
|
+
- File path (str) if output to file.
|
321
|
+
- Backup data as bytes (if output to binary stream) or string (if output to text stream).
|
322
|
+
- None if an error occurs.
|
323
|
+
"""
|
324
|
+
|
325
|
+
host = backup.url.host
|
326
|
+
user = backup.url.username
|
327
|
+
password = backup.url.password
|
328
|
+
database = backup.url.database
|
329
|
+
port = backup.url.port
|
330
|
+
|
331
|
+
if not database:
|
332
|
+
log.error("Error: Database name is required for backup.")
|
333
|
+
return None
|
334
|
+
|
335
|
+
# Build mysqldump command
|
336
|
+
command = [
|
337
|
+
"mysqldump",
|
338
|
+
f"--host={host}",
|
339
|
+
f"--port={port}",
|
340
|
+
f"--user={user}",
|
341
|
+
f"--password={password}",
|
342
|
+
"--single-transaction",
|
343
|
+
"--routines",
|
344
|
+
"--triggers",
|
345
|
+
"--events",
|
346
|
+
"--lock-tables=false", # Often used to avoid locking during backup
|
347
|
+
"--add-drop-table",
|
348
|
+
"--extended-insert",
|
349
|
+
database,
|
350
|
+
]
|
351
|
+
|
352
|
+
def export_to_stream_internal() -> Optional[io.BytesIO]:
|
353
|
+
nonlocal command, compress, database
|
354
|
+
# If streaming, execute mysqldump and capture stdout
|
355
|
+
log.info(f"Backing up database '{database}' to BytesIO...")
|
356
|
+
|
357
|
+
try:
|
358
|
+
if compress:
|
359
|
+
# Start mysqldump process
|
360
|
+
mysqldump_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
361
|
+
|
362
|
+
# Start gzip process, taking input from mysqldump
|
363
|
+
gzip_process = subprocess.Popen(["gzip", "-c"], stdin=mysqldump_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
364
|
+
|
365
|
+
# Close mysqldump stdout in parent process - gzip will handle it
|
366
|
+
mysqldump_process.stdout.close()
|
367
|
+
|
368
|
+
# Wait for gzip to complete (which will also wait for mysqldump)
|
369
|
+
gzip_stdout, gzip_stderr = gzip_process.communicate()
|
370
|
+
|
371
|
+
# Wait for mysqldump to finish and get its stderr
|
372
|
+
mysqldump_stderr = mysqldump_process.communicate()[1]
|
373
|
+
|
374
|
+
if mysqldump_process.returncode != 0:
|
375
|
+
log.error(f"mysqldump error: {mysqldump_stderr.decode().strip()}")
|
376
|
+
return None
|
377
|
+
if gzip_process.returncode != 0:
|
378
|
+
log.error(f"gzip error: {gzip_stderr.decode().strip()}")
|
379
|
+
return None
|
380
|
+
|
381
|
+
log.info("Backup successful and compressed to BytesIO.")
|
382
|
+
return io.BytesIO(gzip_stdout)
|
383
|
+
else:
|
384
|
+
# Directly capture mysqldump output
|
385
|
+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
386
|
+
stdout, stderr = process.communicate()
|
387
|
+
|
388
|
+
if process.returncode != 0:
|
389
|
+
log.error(f"mysqldump error: {stderr.decode().strip()}")
|
390
|
+
return None
|
391
|
+
|
392
|
+
log.info("Backup successful to BytesIO.")
|
393
|
+
return io.BytesIO(stdout)
|
394
|
+
|
395
|
+
except FileNotFoundError as e:
|
396
|
+
log.error(f"Error: '{e.filename}' command not found. Please ensure mysqldump (and gzip if compressing) is installed and in your system's PATH.")
|
397
|
+
return None
|
398
|
+
except Exception as e:
|
399
|
+
log.error(f"An unexpected error occurred during streaming backup: {e}")
|
400
|
+
return None
|
401
|
+
|
402
|
+
def export_to_file_internal(file_path: customString) -> Optional[Path]:
|
403
|
+
nonlocal command, compress, database
|
404
|
+
|
405
|
+
if isinstance(file_path, str):
|
406
|
+
file_path = Path(file_path)
|
407
|
+
|
408
|
+
if not file_path.exists():
|
409
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
410
|
+
file_path.touch()
|
411
|
+
|
412
|
+
try:
|
413
|
+
if compress:
|
414
|
+
# Pipe mysqldump output through gzip to file
|
415
|
+
with open(file_path, "wb") as output_file:
|
416
|
+
# Start mysqldump process
|
417
|
+
mysqldump_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
418
|
+
|
419
|
+
# Start gzip process, taking input from mysqldump and writing to file
|
420
|
+
gzip_process = subprocess.Popen(["gzip", "-c"], stdin=mysqldump_process.stdout, stdout=output_file, stderr=subprocess.PIPE)
|
421
|
+
|
422
|
+
# Close mysqldump stdout in parent process - gzip will handle it
|
423
|
+
mysqldump_process.stdout.close()
|
424
|
+
|
425
|
+
# Wait for gzip to complete (which will also wait for mysqldump)
|
426
|
+
gzip_stdout, gzip_stderr = gzip_process.communicate()
|
427
|
+
|
428
|
+
# Wait for mysqldump to finish and get its stderr
|
429
|
+
mysqldump_stderr = mysqldump_process.communicate()[1]
|
430
|
+
|
431
|
+
if mysqldump_process.returncode != 0:
|
432
|
+
log.error(f"mysqldump error: {mysqldump_stderr.decode().strip()}")
|
433
|
+
return None
|
434
|
+
if gzip_process.returncode != 0:
|
435
|
+
log.error(f"gzip error: {gzip_stderr.decode().strip()}")
|
436
|
+
return None
|
437
|
+
else:
|
438
|
+
# Directly redirect mysqldump output to file
|
439
|
+
with open(file_path, "wb") as output_file:
|
440
|
+
process = subprocess.Popen(command, stdout=output_file, stderr=subprocess.PIPE)
|
441
|
+
stdout, stderr = process.communicate()
|
442
|
+
|
443
|
+
if process.returncode != 0:
|
444
|
+
log.error(f"mysqldump error: {stderr.decode().strip()}")
|
445
|
+
return None
|
446
|
+
|
447
|
+
log.info(f"Backup completed successfully: {file_path}")
|
448
|
+
return file_path
|
449
|
+
|
450
|
+
except FileNotFoundError as e:
|
451
|
+
log.error(f"Error: '{e.filename}' command not found. Please ensure mysqldump (and gzip if compressing) is installed and in your system's PATH.")
|
452
|
+
return None
|
453
|
+
except Exception as e:
|
454
|
+
log.error(f"An unexpected error occurred during file backup: {e}")
|
455
|
+
return None
|
456
|
+
|
457
|
+
try:
|
458
|
+
if output is None:
|
459
|
+
# Auto-generate file path
|
460
|
+
|
461
|
+
backup_dir = Path(backup_dir)
|
462
|
+
backup_dir.mkdir(exist_ok=True)
|
463
|
+
|
464
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
465
|
+
file_extension = "sql.gz" if compress else "sql"
|
466
|
+
output_filename = f"{database}_backup_{timestamp}.{file_extension}"
|
467
|
+
output_filepath = os.path.join(backup_dir, output_filename)
|
468
|
+
return export_to_file_internal(output_filepath)
|
469
|
+
|
470
|
+
elif isinstance(output, (io.BytesIO, io.StringIO)):
|
471
|
+
# Output to a stream object
|
472
|
+
stream_result = export_to_stream_internal()
|
473
|
+
if stream_result:
|
474
|
+
# Write the content from the internal BytesIO to the provided output stream
|
475
|
+
if isinstance(output, io.BytesIO):
|
476
|
+
output.write(stream_result.getvalue())
|
477
|
+
return stream_result.getvalue() # Return bytes if it was a BytesIO internally
|
478
|
+
elif isinstance(output, io.StringIO):
|
479
|
+
# Attempt to decode bytes to string if target is StringIO
|
480
|
+
try:
|
481
|
+
decoded_content = stream_result.getvalue().decode("utf-8")
|
482
|
+
output.write(decoded_content)
|
483
|
+
return decoded_content
|
484
|
+
except UnicodeDecodeError:
|
485
|
+
log.error("Error: Cannot decode byte stream to UTF-8 for StringIO output. Consider setting compress=False or ensuring database encoding is compatible.")
|
486
|
+
return None
|
487
|
+
return None
|
488
|
+
|
489
|
+
elif isinstance(output, str | Path):
|
490
|
+
# Output to a custom file path
|
491
|
+
return export_to_file_internal(output)
|
492
|
+
|
493
|
+
elif isinstance(output, (BinaryIO, TextIO)): # Handles sys.stdout, open file objects
|
494
|
+
stream_result = export_to_stream_internal()
|
495
|
+
if stream_result:
|
496
|
+
if "b" in getattr(output, "mode", "") or isinstance(output, BinaryIO): # Check if it's a binary stream
|
497
|
+
output.write(stream_result.getvalue())
|
498
|
+
return stream_result.getvalue()
|
499
|
+
else: # Assume text stream
|
500
|
+
try:
|
501
|
+
decoded_content = stream_result.getvalue().decode("utf-8")
|
502
|
+
output.write(decoded_content)
|
503
|
+
return decoded_content
|
504
|
+
except UnicodeDecodeError:
|
505
|
+
log.error("Error: Cannot decode byte stream to UTF-8 for text stream output. Consider setting compress=False or ensuring database encoding is compatible.")
|
506
|
+
return None
|
507
|
+
return None
|
508
|
+
|
509
|
+
else:
|
510
|
+
log.error(f"Unsupported output type: {type(output)}")
|
511
|
+
return None
|
512
|
+
|
513
|
+
except Exception as e:
|
514
|
+
log.error(f"An unexpected error occurred: {e}")
|
515
|
+
return None
|
516
|
+
|
517
|
+
|
518
|
+
class GenericTypeCompiler(TypeCompiler):
|
519
|
+
"""Generic type compiler
|
520
|
+
|
521
|
+
This class is used to compile ormlambda types into their
|
522
|
+
string representations for the given dialect.
|
523
|
+
"""
|
524
|
+
|
525
|
+
def _render_string_type(self, type_: STRING, name: str, length_override: Optional[int] = None):
|
526
|
+
text = name
|
527
|
+
if length_override:
|
528
|
+
text += "(%d)" % length_override
|
529
|
+
elif type_.length:
|
530
|
+
text += "(%d)" % type_.length
|
531
|
+
if type_.collation:
|
532
|
+
text += ' COLLATE "%s"' % type_.collation
|
533
|
+
return text
|
534
|
+
|
535
|
+
def visit_INTEGER(self, type_: INTEGER, **kw):
|
536
|
+
return "INTEGER"
|
537
|
+
|
538
|
+
def visit_SMALLINTEGER(self, type_: SMALLINTEGER, **kw):
|
539
|
+
return "SMALLINTEGER"
|
540
|
+
|
541
|
+
def visit_BIGINTEGER(self, type_: BIGINTEGER, **kw):
|
542
|
+
return "BIGINTEGER"
|
543
|
+
|
544
|
+
def visit_NUMERIC(self, type_: NUMERIC, **kw):
|
545
|
+
return "NUMERIC"
|
546
|
+
|
547
|
+
def visit_FLOAT(self, type_: FLOAT, **kw):
|
548
|
+
return "FLOAT"
|
549
|
+
|
550
|
+
def visit_REAL(self, type_: REAL, **kw):
|
551
|
+
return "REAL"
|
552
|
+
|
553
|
+
def visit_DOUBLE(self, type_: DOUBLE, **kw):
|
554
|
+
return "DOUBLE"
|
555
|
+
|
556
|
+
def visit_TEXT(self, type_: TEXT, **kw):
|
557
|
+
return self._render_string_type(type_, "TEXT", **kw)
|
558
|
+
|
559
|
+
def visit_UNICODE(self, type_: UNICODE, **kw):
|
560
|
+
return self._render_string_type(type_, "UNICODE", **kw)
|
561
|
+
|
562
|
+
def visit_UNICODETEXT(self, type_: UNICODETEXT, **kw):
|
563
|
+
return self._render_string_type(type_, "UNICODETEXT", **kw)
|
564
|
+
|
565
|
+
def visit_CHAR(self, type_: CHAR, **kw):
|
566
|
+
return self._render_string_type(type_, "CHAR", **kw)
|
567
|
+
|
568
|
+
def visit_NCHAR(self, type_: NCHAR, **kw):
|
569
|
+
return self._render_string_type(type_, "NCHAR", **kw)
|
570
|
+
|
571
|
+
def visit_VARCHAR(self, type_: VARCHAR, **kw):
|
572
|
+
return self._render_string_type(type_, "VARCHAR", **kw)
|
573
|
+
|
574
|
+
def visit_NVARCHAR(self, type_: NVARCHAR, **kw):
|
575
|
+
return self._render_string_type(type_, "NVARCHAR", **kw)
|
576
|
+
|
577
|
+
def visit_DATE(self, type_: DATE, **kw):
|
578
|
+
return "DATE"
|
579
|
+
|
580
|
+
def visit_TIME(self, type_: TIME, **kw):
|
581
|
+
return "TIME"
|
582
|
+
|
583
|
+
def visit_DATETIME(self, type_: DATETIME, **kw):
|
584
|
+
return "DATETIME"
|
585
|
+
|
586
|
+
def visit_TIMESTAMP(self, type_: TIMESTAMP, **kw):
|
587
|
+
return "TIMESTAMP"
|
588
|
+
|
589
|
+
def visit_BOOLEAN(self, type_: BOOLEAN, **kw):
|
590
|
+
return "BOOLEAN"
|
591
|
+
|
592
|
+
def visit_LARGEBINARY(self, type_: LARGEBINARY, **kw):
|
593
|
+
return "LARGEBINARY"
|
594
|
+
|
595
|
+
def visit_VARBINARY(self, type_: VARBINARY, **kw):
|
596
|
+
return "VARBINARY"
|
597
|
+
|
598
|
+
def visit_ENUM(self, type_: ENUM, **kw):
|
599
|
+
return "ENUM"
|
600
|
+
|
601
|
+
def visit_BLOB(self, type_: LARGEBINARY, **kw):
|
602
|
+
return "BLOB"
|
603
|
+
|
604
|
+
def visit_null(self, type_: POINT, **kw):
|
605
|
+
return "NULL"
|
606
|
+
|
607
|
+
def visit_POINT(self, _type: POINT, **kw):
|
608
|
+
return "POINT"
|
609
|
+
|
610
|
+
def visit_uuid(self, type_, **kw):
|
611
|
+
if not type_.native_uuid or not self.dialect.supports_native_uuid:
|
612
|
+
return self._render_string_type(type_, "CHAR", length_override=32)
|
613
|
+
else:
|
614
|
+
return self.visit_UUID(type_, **kw)
|
615
|
+
|
616
|
+
def visit_large_binary(self, type_, **kw):
|
617
|
+
return self.visit_BLOB(type_, **kw)
|
618
|
+
|
619
|
+
def visit_boolean(self, type_, **kw):
|
620
|
+
return self.visit_BOOLEAN(type_, **kw)
|
621
|
+
|
622
|
+
def visit_time(self, type_, **kw):
|
623
|
+
return self.visit_TIME(type_, **kw)
|
624
|
+
|
625
|
+
def visit_datetime(self, type_, **kw):
|
626
|
+
return self.visit_DATETIME(type_, **kw)
|
627
|
+
|
628
|
+
def visit_date(self, type_, **kw):
|
629
|
+
return self.visit_DATE(type_, **kw)
|
630
|
+
|
631
|
+
def visit_big_integer(self, type_, **kw):
|
632
|
+
return self.visit_BIGINT(type_, **kw)
|
633
|
+
|
634
|
+
def visit_small_integer(self, type_, **kw):
|
635
|
+
return self.visit_SMALLINT(type_, **kw)
|
636
|
+
|
637
|
+
def visit_integer(self, type_, **kw):
|
638
|
+
return self.visit_INTEGER(type_, **kw)
|
639
|
+
|
640
|
+
def visit_real(self, type_, **kw):
|
641
|
+
return self.visit_REAL(type_, **kw)
|
642
|
+
|
643
|
+
def visit_float(self, type_, **kw):
|
644
|
+
return self.visit_FLOAT(type_, **kw)
|
645
|
+
|
646
|
+
def visit_double(self, type_, **kw):
|
647
|
+
return self.visit_DOUBLE(type_, **kw)
|
648
|
+
|
649
|
+
def visit_numeric(self, type_, **kw):
|
650
|
+
return self.visit_NUMERIC(type_, **kw)
|
651
|
+
|
652
|
+
def visit_string(self, type_, **kw):
|
653
|
+
return self.visit_VARCHAR(type_, **kw)
|
654
|
+
|
655
|
+
def visit_unicode(self, type_, **kw):
|
656
|
+
return self.visit_VARCHAR(type_, **kw)
|
657
|
+
|
658
|
+
def visit_text(self, type_, **kw):
|
659
|
+
return self.visit_TEXT(type_, **kw)
|
660
|
+
|
661
|
+
def visit_unicode_text(self, type_, **kw):
|
662
|
+
return self.visit_TEXT(type_, **kw)
|
663
|
+
|
664
|
+
def visit_enum(self, type_, **kw):
|
665
|
+
return self.visit_VARCHAR(type_, **kw)
|
666
|
+
|
667
|
+
def visit_point(self, type_: POINT, **kw):
|
668
|
+
return self.visit_POINT(type_, **kw)
|