ormlambda 3.11.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.
Files changed (154) hide show
  1. ormlambda/__init__.py +3 -1
  2. ormlambda/caster/__init__.py +1 -1
  3. ormlambda/caster/caster.py +29 -12
  4. ormlambda/common/abstract_classes/clause_info_converter.py +65 -0
  5. ormlambda/common/abstract_classes/decomposition_query.py +27 -68
  6. ormlambda/common/abstract_classes/non_query_base.py +10 -8
  7. ormlambda/common/abstract_classes/query_base.py +3 -1
  8. ormlambda/common/errors/__init__.py +29 -0
  9. ormlambda/common/interfaces/ICustomAlias.py +1 -1
  10. ormlambda/common/interfaces/IQueryCommand.py +6 -2
  11. ormlambda/dialects/__init__.py +39 -0
  12. ormlambda/dialects/default/__init__.py +1 -0
  13. ormlambda/dialects/default/base.py +39 -0
  14. ormlambda/dialects/interface/__init__.py +1 -0
  15. ormlambda/dialects/interface/dialect.py +78 -0
  16. ormlambda/dialects/mysql/__init__.py +38 -0
  17. ormlambda/dialects/mysql/base.py +388 -0
  18. ormlambda/dialects/mysql/caster/caster.py +39 -0
  19. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/__init__.py +1 -0
  20. ormlambda/dialects/mysql/caster/types/boolean.py +35 -0
  21. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/bytes.py +7 -7
  22. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/datetime.py +7 -7
  23. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/float.py +7 -7
  24. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/int.py +7 -7
  25. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/iterable.py +7 -7
  26. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/none.py +8 -7
  27. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/point.py +4 -4
  28. ormlambda/{databases/my_sql → dialects/mysql}/caster/types/string.py +7 -7
  29. ormlambda/{databases/my_sql → dialects/mysql}/clauses/ST_AsText.py +8 -7
  30. ormlambda/{databases/my_sql → dialects/mysql}/clauses/ST_Contains.py +10 -5
  31. ormlambda/dialects/mysql/clauses/__init__.py +13 -0
  32. ormlambda/dialects/mysql/clauses/count.py +33 -0
  33. ormlambda/dialects/mysql/clauses/delete.py +9 -0
  34. ormlambda/dialects/mysql/clauses/group_by.py +17 -0
  35. ormlambda/dialects/mysql/clauses/having.py +12 -0
  36. ormlambda/dialects/mysql/clauses/insert.py +9 -0
  37. ormlambda/dialects/mysql/clauses/joins.py +14 -0
  38. ormlambda/dialects/mysql/clauses/limit.py +6 -0
  39. ormlambda/dialects/mysql/clauses/offset.py +6 -0
  40. ormlambda/dialects/mysql/clauses/order.py +8 -0
  41. ormlambda/dialects/mysql/clauses/update.py +8 -0
  42. ormlambda/dialects/mysql/clauses/upsert.py +9 -0
  43. ormlambda/dialects/mysql/clauses/where.py +7 -0
  44. ormlambda/dialects/mysql/mysqlconnector.py +46 -0
  45. ormlambda/dialects/mysql/repository/__init__.py +1 -0
  46. ormlambda/dialects/mysql/repository/repository.py +212 -0
  47. ormlambda/dialects/mysql/types.py +732 -0
  48. ormlambda/dialects/sqlite/__init__.py +5 -0
  49. ormlambda/dialects/sqlite/base.py +47 -0
  50. ormlambda/dialects/sqlite/pysqlite.py +32 -0
  51. ormlambda/engine/__init__.py +1 -0
  52. ormlambda/engine/base.py +77 -0
  53. ormlambda/engine/create.py +9 -23
  54. ormlambda/engine/url.py +34 -19
  55. ormlambda/env.py +30 -0
  56. ormlambda/errors.py +17 -0
  57. ormlambda/model/base_model.py +7 -9
  58. ormlambda/repository/base_repository.py +36 -5
  59. ormlambda/repository/interfaces/IRepositoryBase.py +119 -12
  60. ormlambda/repository/response.py +134 -0
  61. ormlambda/sql/clause_info/__init__.py +2 -1
  62. ormlambda/sql/clause_info/aggregate_function_base.py +96 -0
  63. ormlambda/sql/clause_info/clause_info.py +35 -115
  64. ormlambda/sql/clause_info/interface/IClauseInfo.py +37 -0
  65. ormlambda/sql/clause_info/interface/__init__.py +1 -0
  66. ormlambda/sql/clauses/__init__.py +14 -0
  67. ormlambda/{databases/my_sql → sql}/clauses/alias.py +23 -6
  68. ormlambda/{databases/my_sql → sql}/clauses/count.py +15 -1
  69. ormlambda/{databases/my_sql → sql}/clauses/delete.py +22 -7
  70. ormlambda/sql/clauses/group_by.py +30 -0
  71. ormlambda/{databases/my_sql → sql}/clauses/having.py +7 -2
  72. ormlambda/{databases/my_sql → sql}/clauses/insert.py +16 -9
  73. ormlambda/sql/clauses/interfaces/__init__.py +5 -0
  74. ormlambda/sql/clauses/join/__init__.py +1 -0
  75. ormlambda/{databases/my_sql → sql/clauses/join}/join_context.py +15 -7
  76. ormlambda/{databases/my_sql → sql}/clauses/joins.py +29 -19
  77. ormlambda/sql/clauses/limit.py +15 -0
  78. ormlambda/sql/clauses/offset.py +15 -0
  79. ormlambda/{databases/my_sql → sql}/clauses/order.py +14 -24
  80. ormlambda/{databases/my_sql → sql}/clauses/select.py +14 -13
  81. ormlambda/{databases/my_sql → sql}/clauses/update.py +24 -11
  82. ormlambda/{databases/my_sql → sql}/clauses/upsert.py +19 -10
  83. ormlambda/{databases/my_sql → sql}/clauses/where.py +28 -8
  84. ormlambda/sql/column/__init__.py +1 -0
  85. ormlambda/sql/{column.py → column/column.py} +85 -22
  86. ormlambda/sql/comparer.py +51 -37
  87. ormlambda/sql/compiler.py +668 -0
  88. ormlambda/sql/ddl.py +82 -0
  89. ormlambda/sql/elements.py +36 -0
  90. ormlambda/sql/foreign_key.py +61 -39
  91. ormlambda/{databases/my_sql → sql}/functions/concat.py +13 -5
  92. ormlambda/{databases/my_sql → sql}/functions/max.py +9 -4
  93. ormlambda/{databases/my_sql → sql}/functions/min.py +9 -13
  94. ormlambda/{databases/my_sql → sql}/functions/sum.py +8 -10
  95. ormlambda/sql/sqltypes.py +647 -0
  96. ormlambda/sql/table/__init__.py +1 -1
  97. ormlambda/sql/table/table.py +175 -0
  98. ormlambda/sql/table/table_constructor.py +1 -208
  99. ormlambda/sql/type_api.py +35 -0
  100. ormlambda/sql/types.py +3 -1
  101. ormlambda/sql/visitors.py +74 -0
  102. ormlambda/statements/__init__.py +1 -0
  103. ormlambda/statements/base_statement.py +34 -40
  104. ormlambda/statements/interfaces/IStatements.py +28 -21
  105. ormlambda/statements/query_builder.py +163 -0
  106. ormlambda/{databases/my_sql → statements}/statements.py +68 -210
  107. ormlambda/statements/types.py +2 -2
  108. ormlambda/types/__init__.py +24 -0
  109. ormlambda/types/metadata.py +42 -0
  110. ormlambda/util/__init__.py +87 -0
  111. ormlambda/{utils → util}/module_tree/dynamic_module.py +4 -3
  112. ormlambda/util/plugin_loader.py +32 -0
  113. ormlambda/util/typing.py +6 -0
  114. ormlambda-3.34.0.dist-info/AUTHORS +32 -0
  115. {ormlambda-3.11.2.dist-info → ormlambda-3.34.0.dist-info}/METADATA +56 -10
  116. ormlambda-3.34.0.dist-info/RECORD +152 -0
  117. ormlambda/components/__init__.py +0 -4
  118. ormlambda/components/delete/__init__.py +0 -2
  119. ormlambda/components/delete/abstract_delete.py +0 -17
  120. ormlambda/components/insert/__init__.py +0 -2
  121. ormlambda/components/insert/abstract_insert.py +0 -25
  122. ormlambda/components/select/__init__.py +0 -1
  123. ormlambda/components/update/__init__.py +0 -2
  124. ormlambda/components/update/abstract_update.py +0 -29
  125. ormlambda/components/upsert/__init__.py +0 -2
  126. ormlambda/components/upsert/abstract_upsert.py +0 -25
  127. ormlambda/databases/__init__.py +0 -5
  128. ormlambda/databases/my_sql/__init__.py +0 -4
  129. ormlambda/databases/my_sql/caster/caster.py +0 -39
  130. ormlambda/databases/my_sql/clauses/__init__.py +0 -20
  131. ormlambda/databases/my_sql/clauses/create_database.py +0 -35
  132. ormlambda/databases/my_sql/clauses/drop_database.py +0 -17
  133. ormlambda/databases/my_sql/clauses/drop_table.py +0 -23
  134. ormlambda/databases/my_sql/clauses/group_by.py +0 -31
  135. ormlambda/databases/my_sql/clauses/limit.py +0 -17
  136. ormlambda/databases/my_sql/clauses/offset.py +0 -17
  137. ormlambda/databases/my_sql/repository/__init__.py +0 -1
  138. ormlambda/databases/my_sql/repository/repository.py +0 -351
  139. ormlambda/engine/template.py +0 -47
  140. ormlambda/sql/dtypes.py +0 -94
  141. ormlambda/utils/__init__.py +0 -1
  142. ormlambda-3.11.2.dist-info/RECORD +0 -120
  143. /ormlambda/{databases/my_sql → dialects/mysql}/caster/__init__.py +0 -0
  144. /ormlambda/{databases/my_sql/types.py → dialects/mysql/repository/pool_types.py} +0 -0
  145. /ormlambda/{components/delete → sql/clauses/interfaces}/IDelete.py +0 -0
  146. /ormlambda/{components/insert → sql/clauses/interfaces}/IInsert.py +0 -0
  147. /ormlambda/{components/select → sql/clauses/interfaces}/ISelect.py +0 -0
  148. /ormlambda/{components/update → sql/clauses/interfaces}/IUpdate.py +0 -0
  149. /ormlambda/{components/upsert → sql/clauses/interfaces}/IUpsert.py +0 -0
  150. /ormlambda/{databases/my_sql → sql}/functions/__init__.py +0 -0
  151. /ormlambda/{utils → util}/module_tree/__init__.py +0 -0
  152. /ormlambda/{utils → util}/module_tree/dfs_traversal.py +0 -0
  153. {ormlambda-3.11.2.dist-info → ormlambda-3.34.0.dist-info}/LICENSE +0 -0
  154. {ormlambda-3.11.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)