velocity-python 0.0.109__py3-none-any.whl → 0.0.161__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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +248 -0
- velocity/aws/handlers/context.py +251 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/__init__.py +16 -4
- velocity/db/core/decorators.py +48 -13
- velocity/db/core/engine.py +187 -840
- velocity/db/core/result.py +33 -25
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +493 -50
- velocity/db/core/transaction.py +28 -15
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +270 -0
- velocity/db/tests/test_postgres.py +448 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/db/utils.py +129 -51
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/METADATA +2 -2
- velocity_python-0.0.161.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.109.dist-info/RECORD +0 -56
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import hashlib
|
|
3
|
+
import decimal
|
|
4
|
+
import datetime
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from velocity.db import exceptions
|
|
9
|
+
from ..base.sql import BaseSQLDialect
|
|
10
|
+
from .reserved import reserved_words
|
|
11
|
+
from .types import TYPES
|
|
12
|
+
from .operators import OPERATORS, MySQLOperators
|
|
13
|
+
from ..tablehelper import TableHelper
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Configure TableHelper for MySQL
|
|
17
|
+
TableHelper.reserved = reserved_words
|
|
18
|
+
TableHelper.operators = OPERATORS
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
system_fields = [
|
|
22
|
+
"sys_id",
|
|
23
|
+
"sys_created",
|
|
24
|
+
"sys_modified",
|
|
25
|
+
"sys_modified_by",
|
|
26
|
+
"sys_dirty",
|
|
27
|
+
"sys_table",
|
|
28
|
+
"sys_modified_count",
|
|
29
|
+
"description",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def quote(data):
|
|
34
|
+
"""Quote MySQL identifiers."""
|
|
35
|
+
if isinstance(data, list):
|
|
36
|
+
return [quote(item) for item in data]
|
|
37
|
+
else:
|
|
38
|
+
parts = data.split(".")
|
|
39
|
+
new = []
|
|
40
|
+
for part in parts:
|
|
41
|
+
if "`" in part:
|
|
42
|
+
new.append(part)
|
|
43
|
+
elif part.upper() in reserved_words:
|
|
44
|
+
new.append("`" + part + "`")
|
|
45
|
+
elif re.findall("[/]", part):
|
|
46
|
+
new.append("`" + part + "`")
|
|
47
|
+
else:
|
|
48
|
+
new.append(part)
|
|
49
|
+
return ".".join(new)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SQL(BaseSQLDialect):
|
|
53
|
+
server = "MySQL"
|
|
54
|
+
type_column_identifier = "DATA_TYPE"
|
|
55
|
+
is_nullable = "IS_NULLABLE"
|
|
56
|
+
|
|
57
|
+
default_schema = ""
|
|
58
|
+
|
|
59
|
+
ApplicationErrorCodes = []
|
|
60
|
+
DatabaseMissingErrorCodes = ["1049"] # ER_BAD_DB_ERROR
|
|
61
|
+
TableMissingErrorCodes = ["1146"] # ER_NO_SUCH_TABLE
|
|
62
|
+
ColumnMissingErrorCodes = ["1054"] # ER_BAD_FIELD_ERROR
|
|
63
|
+
ForeignKeyMissingErrorCodes = ["1005"] # ER_CANT_CREATE_TABLE
|
|
64
|
+
ConnectionErrorCodes = ["2002", "2003", "2006"] # Connection errors
|
|
65
|
+
DuplicateKeyErrorCodes = ["1062"] # ER_DUP_ENTRY
|
|
66
|
+
RetryTransactionCodes = ["1213"] # ER_LOCK_DEADLOCK
|
|
67
|
+
TruncationErrorCodes = ["1406"] # ER_DATA_TOO_LONG
|
|
68
|
+
LockTimeoutErrorCodes = ["1205"] # ER_LOCK_WAIT_TIMEOUT
|
|
69
|
+
DatabaseObjectExistsErrorCodes = ["1050"] # ER_TABLE_EXISTS_ERROR
|
|
70
|
+
DataIntegrityErrorCodes = ["1452", "1048", "1364"] # Foreign key, null, no default
|
|
71
|
+
|
|
72
|
+
types = TYPES
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_error(cls, e):
|
|
76
|
+
"""Extract error information from MySQL exception."""
|
|
77
|
+
error_code = getattr(e, "errno", None)
|
|
78
|
+
error_msg = getattr(e, "msg", None)
|
|
79
|
+
return error_code, error_msg
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def select(
|
|
83
|
+
cls,
|
|
84
|
+
tx,
|
|
85
|
+
columns=None,
|
|
86
|
+
table=None,
|
|
87
|
+
where=None,
|
|
88
|
+
orderby=None,
|
|
89
|
+
groupby=None,
|
|
90
|
+
having=None,
|
|
91
|
+
start=None,
|
|
92
|
+
qty=None,
|
|
93
|
+
lock=None,
|
|
94
|
+
skip_locked=None,
|
|
95
|
+
):
|
|
96
|
+
"""Generate a MySQL SELECT statement."""
|
|
97
|
+
if not table:
|
|
98
|
+
raise ValueError("Table name is required")
|
|
99
|
+
|
|
100
|
+
sql_parts = []
|
|
101
|
+
vals = []
|
|
102
|
+
|
|
103
|
+
# SELECT clause
|
|
104
|
+
if columns is None:
|
|
105
|
+
columns = ["*"]
|
|
106
|
+
elif isinstance(columns, str):
|
|
107
|
+
columns = [columns]
|
|
108
|
+
|
|
109
|
+
sql_parts.append("SELECT")
|
|
110
|
+
sql_parts.append(", ".join(columns))
|
|
111
|
+
|
|
112
|
+
# FROM clause
|
|
113
|
+
sql_parts.append("FROM")
|
|
114
|
+
sql_parts.append(quote(table))
|
|
115
|
+
|
|
116
|
+
# WHERE clause
|
|
117
|
+
if where:
|
|
118
|
+
where_sql, where_vals = cls._build_where(where)
|
|
119
|
+
sql_parts.append("WHERE")
|
|
120
|
+
sql_parts.append(where_sql)
|
|
121
|
+
vals.extend(where_vals)
|
|
122
|
+
|
|
123
|
+
# GROUP BY clause
|
|
124
|
+
if groupby:
|
|
125
|
+
if isinstance(groupby, str):
|
|
126
|
+
groupby = [groupby]
|
|
127
|
+
sql_parts.append("GROUP BY")
|
|
128
|
+
sql_parts.append(", ".join(quote(col) for col in groupby))
|
|
129
|
+
|
|
130
|
+
# HAVING clause
|
|
131
|
+
if having:
|
|
132
|
+
having_sql, having_vals = cls._build_where(having)
|
|
133
|
+
sql_parts.append("HAVING")
|
|
134
|
+
sql_parts.append(having_sql)
|
|
135
|
+
vals.extend(having_vals)
|
|
136
|
+
|
|
137
|
+
# ORDER BY clause
|
|
138
|
+
if orderby:
|
|
139
|
+
if isinstance(orderby, str):
|
|
140
|
+
orderby = [orderby]
|
|
141
|
+
elif isinstance(orderby, dict):
|
|
142
|
+
orderby_list = []
|
|
143
|
+
for col, direction in orderby.items():
|
|
144
|
+
orderby_list.append(f"{quote(col)} {direction.upper()}")
|
|
145
|
+
orderby = orderby_list
|
|
146
|
+
sql_parts.append("ORDER BY")
|
|
147
|
+
sql_parts.append(", ".join(orderby))
|
|
148
|
+
|
|
149
|
+
# LIMIT clause (MySQL uses LIMIT instead of OFFSET/FETCH)
|
|
150
|
+
if start is not None and qty is not None:
|
|
151
|
+
sql_parts.append(f"LIMIT {start}, {qty}")
|
|
152
|
+
elif qty is not None:
|
|
153
|
+
sql_parts.append(f"LIMIT {qty}")
|
|
154
|
+
|
|
155
|
+
# FOR UPDATE (lock)
|
|
156
|
+
if lock:
|
|
157
|
+
sql_parts.append("FOR UPDATE")
|
|
158
|
+
if skip_locked:
|
|
159
|
+
sql_parts.append("SKIP LOCKED")
|
|
160
|
+
|
|
161
|
+
return " ".join(sql_parts), vals
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def _build_where(cls, where):
|
|
165
|
+
"""Build WHERE clause for MySQL."""
|
|
166
|
+
if isinstance(where, str):
|
|
167
|
+
return where, []
|
|
168
|
+
|
|
169
|
+
if isinstance(where, dict):
|
|
170
|
+
where = list(where.items())
|
|
171
|
+
|
|
172
|
+
if not isinstance(where, (list, tuple)):
|
|
173
|
+
raise ValueError("WHERE clause must be string, dict, or list")
|
|
174
|
+
|
|
175
|
+
conditions = []
|
|
176
|
+
vals = []
|
|
177
|
+
|
|
178
|
+
for key, val in where:
|
|
179
|
+
if val is None:
|
|
180
|
+
if "!" in key:
|
|
181
|
+
key = key.replace("!", "")
|
|
182
|
+
conditions.append(f"{quote(key)} IS NOT NULL")
|
|
183
|
+
else:
|
|
184
|
+
conditions.append(f"{quote(key)} IS NULL")
|
|
185
|
+
elif isinstance(val, (list, tuple)):
|
|
186
|
+
if "!" in key:
|
|
187
|
+
key = key.replace("!", "")
|
|
188
|
+
conditions.append(f"{quote(key)} NOT IN ({', '.join(['%s'] * len(val))})")
|
|
189
|
+
else:
|
|
190
|
+
conditions.append(f"{quote(key)} IN ({', '.join(['%s'] * len(val))})")
|
|
191
|
+
vals.extend(val)
|
|
192
|
+
else:
|
|
193
|
+
# Handle operators
|
|
194
|
+
op = "="
|
|
195
|
+
if "<>" in key:
|
|
196
|
+
key = key.replace("<>", "")
|
|
197
|
+
op = "<>"
|
|
198
|
+
elif "!=" in key:
|
|
199
|
+
key = key.replace("!=", "")
|
|
200
|
+
op = "<>"
|
|
201
|
+
elif "%%" in key:
|
|
202
|
+
key = key.replace("%%", "")
|
|
203
|
+
op = "LIKE"
|
|
204
|
+
elif "%" in key:
|
|
205
|
+
key = key.replace("%", "")
|
|
206
|
+
op = "LIKE"
|
|
207
|
+
elif "!" in key:
|
|
208
|
+
key = key.replace("!", "")
|
|
209
|
+
op = "<>"
|
|
210
|
+
|
|
211
|
+
conditions.append(f"{quote(key)} {op} %s")
|
|
212
|
+
vals.append(val)
|
|
213
|
+
|
|
214
|
+
return " AND ".join(conditions), vals
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def insert(cls, table, data):
|
|
218
|
+
"""Generate an INSERT statement for MySQL."""
|
|
219
|
+
if not data:
|
|
220
|
+
raise ValueError("Data cannot be empty")
|
|
221
|
+
|
|
222
|
+
columns = list(data.keys())
|
|
223
|
+
values = list(data.values())
|
|
224
|
+
|
|
225
|
+
sql_parts = [
|
|
226
|
+
"INSERT INTO",
|
|
227
|
+
quote(table),
|
|
228
|
+
f"({', '.join(quote(col) for col in columns)})",
|
|
229
|
+
"VALUES",
|
|
230
|
+
f"({', '.join(['%s'] * len(values))})"
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
return " ".join(sql_parts), values
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def update(cls, tx, table, data, where=None, pk=None, excluded=False):
|
|
237
|
+
"""Generate an UPDATE statement for MySQL."""
|
|
238
|
+
if not data:
|
|
239
|
+
raise ValueError("Data cannot be empty")
|
|
240
|
+
|
|
241
|
+
if not where and not pk:
|
|
242
|
+
raise ValueError("Either WHERE clause or primary key must be provided")
|
|
243
|
+
|
|
244
|
+
# Build SET clause
|
|
245
|
+
set_clauses = []
|
|
246
|
+
vals = []
|
|
247
|
+
|
|
248
|
+
for col, val in data.items():
|
|
249
|
+
if excluded:
|
|
250
|
+
# For ON DUPLICATE KEY UPDATE
|
|
251
|
+
set_clauses.append(f"{quote(col)} = VALUES({quote(col)})")
|
|
252
|
+
else:
|
|
253
|
+
set_clauses.append(f"{quote(col)} = %s")
|
|
254
|
+
vals.append(val)
|
|
255
|
+
|
|
256
|
+
# Build WHERE clause
|
|
257
|
+
if pk:
|
|
258
|
+
if where:
|
|
259
|
+
# Merge pk into where
|
|
260
|
+
if isinstance(where, dict):
|
|
261
|
+
where.update(pk)
|
|
262
|
+
else:
|
|
263
|
+
# Convert to dict for merging
|
|
264
|
+
where_dict = dict(where) if isinstance(where, (list, tuple)) else {}
|
|
265
|
+
where_dict.update(pk)
|
|
266
|
+
where = where_dict
|
|
267
|
+
else:
|
|
268
|
+
where = pk
|
|
269
|
+
|
|
270
|
+
where_sql, where_vals = cls._build_where(where) if where else ("", [])
|
|
271
|
+
|
|
272
|
+
sql_parts = [
|
|
273
|
+
"UPDATE",
|
|
274
|
+
quote(table),
|
|
275
|
+
"SET",
|
|
276
|
+
", ".join(set_clauses)
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
if where_sql:
|
|
280
|
+
sql_parts.extend(["WHERE", where_sql])
|
|
281
|
+
vals.extend(where_vals)
|
|
282
|
+
|
|
283
|
+
return " ".join(sql_parts), vals
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def delete(cls, tx, table, where):
|
|
287
|
+
"""Generate a DELETE statement for MySQL."""
|
|
288
|
+
if not where:
|
|
289
|
+
raise ValueError("WHERE clause is required for DELETE")
|
|
290
|
+
|
|
291
|
+
where_sql, where_vals = cls._build_where(where)
|
|
292
|
+
|
|
293
|
+
sql_parts = [
|
|
294
|
+
"DELETE FROM",
|
|
295
|
+
quote(table),
|
|
296
|
+
"WHERE",
|
|
297
|
+
where_sql
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
return " ".join(sql_parts), where_vals
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
|
|
304
|
+
"""Generate an INSERT ... ON DUPLICATE KEY UPDATE statement for MySQL."""
|
|
305
|
+
# First, create the INSERT part
|
|
306
|
+
insert_sql, insert_vals = cls.insert(table, data)
|
|
307
|
+
|
|
308
|
+
if on_conflict_do_nothing:
|
|
309
|
+
# MySQL: INSERT IGNORE
|
|
310
|
+
insert_sql = insert_sql.replace("INSERT INTO", "INSERT IGNORE INTO")
|
|
311
|
+
return insert_sql, insert_vals
|
|
312
|
+
elif on_conflict_update:
|
|
313
|
+
# MySQL: INSERT ... ON DUPLICATE KEY UPDATE
|
|
314
|
+
update_clauses = []
|
|
315
|
+
for col in data.keys():
|
|
316
|
+
if col not in pk: # Don't update primary key columns
|
|
317
|
+
update_clauses.append(f"{quote(col)} = VALUES({quote(col)})")
|
|
318
|
+
|
|
319
|
+
if update_clauses:
|
|
320
|
+
insert_sql += f" ON DUPLICATE KEY UPDATE {', '.join(update_clauses)}"
|
|
321
|
+
|
|
322
|
+
return insert_sql, insert_vals
|
|
323
|
+
else:
|
|
324
|
+
return insert_sql, insert_vals
|
|
325
|
+
|
|
326
|
+
# Metadata queries
|
|
327
|
+
@classmethod
|
|
328
|
+
def version(cls):
|
|
329
|
+
return "SELECT VERSION()"
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def timestamp(cls):
|
|
333
|
+
return "SELECT NOW()"
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def user(cls):
|
|
337
|
+
return "SELECT USER()"
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
def databases(cls):
|
|
341
|
+
return "SHOW DATABASES"
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def schemas(cls):
|
|
345
|
+
return "SHOW DATABASES" # MySQL databases are schemas
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def current_schema(cls):
|
|
349
|
+
return "SELECT DATABASE()"
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def current_database(cls):
|
|
353
|
+
return "SELECT DATABASE()"
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def tables(cls, system=False):
|
|
357
|
+
if system:
|
|
358
|
+
return "SHOW TABLES"
|
|
359
|
+
else:
|
|
360
|
+
return "SHOW TABLES"
|
|
361
|
+
|
|
362
|
+
@classmethod
|
|
363
|
+
def views(cls, system=False):
|
|
364
|
+
return "SHOW FULL TABLES WHERE Table_type = 'VIEW'"
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def create_database(cls, name):
|
|
368
|
+
return f"CREATE DATABASE {quote(name)}"
|
|
369
|
+
|
|
370
|
+
@classmethod
|
|
371
|
+
def drop_database(cls, name):
|
|
372
|
+
return f"DROP DATABASE {quote(name)}"
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def create_table(cls, name, columns=None, drop=False):
|
|
376
|
+
if not name or not isinstance(name, str):
|
|
377
|
+
raise ValueError("Table name must be a non-empty string")
|
|
378
|
+
|
|
379
|
+
columns = columns or {}
|
|
380
|
+
table_identifier = quote(name)
|
|
381
|
+
base_name = name.split(".")[-1].replace("`", "")
|
|
382
|
+
base_name_sql = base_name.replace("'", "''")
|
|
383
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
|
|
384
|
+
|
|
385
|
+
statements = []
|
|
386
|
+
if drop:
|
|
387
|
+
statements.append(f"DROP TABLE IF EXISTS {table_identifier};")
|
|
388
|
+
|
|
389
|
+
statements.append(
|
|
390
|
+
f"""
|
|
391
|
+
CREATE TABLE {table_identifier} (
|
|
392
|
+
`sys_id` BIGINT NOT NULL AUTO_INCREMENT,
|
|
393
|
+
`sys_table` TEXT,
|
|
394
|
+
`sys_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
395
|
+
`sys_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
396
|
+
`sys_modified_by` TEXT,
|
|
397
|
+
`sys_modified_count` INT NOT NULL DEFAULT 0,
|
|
398
|
+
`sys_dirty` TINYINT(1) NOT NULL DEFAULT 0,
|
|
399
|
+
`description` TEXT,
|
|
400
|
+
PRIMARY KEY (`sys_id`)
|
|
401
|
+
) ENGINE=InnoDB;
|
|
402
|
+
""".strip()
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
for key, val in columns.items():
|
|
406
|
+
clean_key = re.sub("<>!=%", "", key)
|
|
407
|
+
if clean_key in system_fields:
|
|
408
|
+
continue
|
|
409
|
+
col_type = TYPES.get_type(val)
|
|
410
|
+
statements.append(
|
|
411
|
+
f"ALTER TABLE {table_identifier} ADD COLUMN {quote(clean_key)} {col_type};"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
statements.extend(
|
|
415
|
+
[
|
|
416
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_bi;",
|
|
417
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_bu;",
|
|
418
|
+
f"""
|
|
419
|
+
CREATE TRIGGER {trigger_prefix}_bi
|
|
420
|
+
BEFORE INSERT ON {table_identifier}
|
|
421
|
+
FOR EACH ROW
|
|
422
|
+
BEGIN
|
|
423
|
+
SET NEW.sys_created = COALESCE(NEW.sys_created, NOW());
|
|
424
|
+
SET NEW.sys_modified = NOW();
|
|
425
|
+
SET NEW.sys_modified_count = 0;
|
|
426
|
+
SET NEW.sys_dirty = IFNULL(NEW.sys_dirty, 0);
|
|
427
|
+
SET NEW.sys_table = '{base_name_sql}';
|
|
428
|
+
END;
|
|
429
|
+
""".strip(),
|
|
430
|
+
f"""
|
|
431
|
+
CREATE TRIGGER {trigger_prefix}_bu
|
|
432
|
+
BEFORE UPDATE ON {table_identifier}
|
|
433
|
+
FOR EACH ROW
|
|
434
|
+
BEGIN
|
|
435
|
+
IF OLD.sys_dirty = TRUE AND NEW.sys_dirty = FALSE THEN
|
|
436
|
+
SET NEW.sys_dirty = 0;
|
|
437
|
+
SET NEW.sys_modified_count = IFNULL(OLD.sys_modified_count, 0);
|
|
438
|
+
ELSE
|
|
439
|
+
SET NEW.sys_dirty = 1;
|
|
440
|
+
SET NEW.sys_modified_count = IFNULL(OLD.sys_modified_count, 0) + 1;
|
|
441
|
+
END IF;
|
|
442
|
+
SET NEW.sys_created = OLD.sys_created;
|
|
443
|
+
SET NEW.sys_modified = NOW();
|
|
444
|
+
SET NEW.sys_table = '{base_name_sql}';
|
|
445
|
+
END;
|
|
446
|
+
""".strip(),
|
|
447
|
+
]
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return "\n".join(statements), tuple()
|
|
451
|
+
|
|
452
|
+
@classmethod
|
|
453
|
+
def ensure_system_columns(cls, name, existing_columns=None, force=False):
|
|
454
|
+
"""Ensure MySQL tables maintain the Velocity system metadata."""
|
|
455
|
+
existing_columns = {col.lower() for col in existing_columns or []}
|
|
456
|
+
|
|
457
|
+
table_identifier = quote(name)
|
|
458
|
+
base_name = name.split(".")[-1].replace("`", "")
|
|
459
|
+
base_name_sql = base_name.replace("'", "''")
|
|
460
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
|
|
461
|
+
|
|
462
|
+
has_count = "sys_modified_count" in existing_columns
|
|
463
|
+
|
|
464
|
+
add_column = not has_count
|
|
465
|
+
recreate_triggers = force or add_column
|
|
466
|
+
|
|
467
|
+
if not recreate_triggers and not force:
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
statements = []
|
|
471
|
+
|
|
472
|
+
if add_column:
|
|
473
|
+
statements.append(
|
|
474
|
+
f"ALTER TABLE {table_identifier} ADD COLUMN IF NOT EXISTS `sys_modified_count` INT NOT NULL DEFAULT 0;"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
statements.append(
|
|
478
|
+
f"UPDATE {table_identifier} SET `sys_modified_count` = 0 WHERE `sys_modified_count` IS NULL;"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_bi;")
|
|
482
|
+
statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_bu;")
|
|
483
|
+
|
|
484
|
+
statements.extend(
|
|
485
|
+
[
|
|
486
|
+
f"""
|
|
487
|
+
CREATE TRIGGER {trigger_prefix}_bi
|
|
488
|
+
BEFORE INSERT ON {table_identifier}
|
|
489
|
+
FOR EACH ROW
|
|
490
|
+
BEGIN
|
|
491
|
+
SET NEW.sys_created = COALESCE(NEW.sys_created, NOW());
|
|
492
|
+
SET NEW.sys_modified = NOW();
|
|
493
|
+
SET NEW.sys_modified_count = 0;
|
|
494
|
+
SET NEW.sys_dirty = IFNULL(NEW.sys_dirty, 0);
|
|
495
|
+
SET NEW.sys_table = '{base_name_sql}';
|
|
496
|
+
END;
|
|
497
|
+
""".strip(),
|
|
498
|
+
f"""
|
|
499
|
+
CREATE TRIGGER {trigger_prefix}_bu
|
|
500
|
+
BEFORE UPDATE ON {table_identifier}
|
|
501
|
+
FOR EACH ROW
|
|
502
|
+
BEGIN
|
|
503
|
+
IF OLD.sys_dirty = TRUE AND NEW.sys_dirty = FALSE THEN
|
|
504
|
+
SET NEW.sys_dirty = 0;
|
|
505
|
+
SET NEW.sys_modified_count = IFNULL(OLD.sys_modified_count, 0);
|
|
506
|
+
ELSE
|
|
507
|
+
SET NEW.sys_dirty = 1;
|
|
508
|
+
SET NEW.sys_modified_count = IFNULL(OLD.sys_modified_count, 0) + 1;
|
|
509
|
+
END IF;
|
|
510
|
+
SET NEW.sys_created = OLD.sys_created;
|
|
511
|
+
SET NEW.sys_modified = NOW();
|
|
512
|
+
SET NEW.sys_table = '{base_name_sql}';
|
|
513
|
+
END;
|
|
514
|
+
""".strip(),
|
|
515
|
+
]
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
return "\n".join(statements), tuple()
|
|
519
|
+
|
|
520
|
+
@classmethod
|
|
521
|
+
def drop_table(cls, name):
|
|
522
|
+
return f"DROP TABLE {quote(name)}"
|
|
523
|
+
|
|
524
|
+
@classmethod
|
|
525
|
+
def truncate(cls, table):
|
|
526
|
+
return f"TRUNCATE TABLE {quote(table)}"
|
|
527
|
+
|
|
528
|
+
@classmethod
|
|
529
|
+
def columns(cls, name):
|
|
530
|
+
return f"SHOW COLUMNS FROM {quote(name)}"
|
|
531
|
+
|
|
532
|
+
@classmethod
|
|
533
|
+
def column_info(cls, table, name):
|
|
534
|
+
return f"SHOW COLUMNS FROM {quote(table)} LIKE '{name}'"
|
|
535
|
+
|
|
536
|
+
@classmethod
|
|
537
|
+
def drop_column(cls, table, name, cascade=True):
|
|
538
|
+
return f"ALTER TABLE {quote(table)} DROP COLUMN {quote(name)}"
|
|
539
|
+
|
|
540
|
+
@classmethod
|
|
541
|
+
def alter_add(cls, table, columns, null_allowed=True):
|
|
542
|
+
alter_parts = []
|
|
543
|
+
for col, col_type in columns.items():
|
|
544
|
+
null_clause = "NULL" if null_allowed else "NOT NULL"
|
|
545
|
+
alter_parts.append(f"ADD COLUMN {quote(col)} {col_type} {null_clause}")
|
|
546
|
+
|
|
547
|
+
return f"ALTER TABLE {quote(table)} {', '.join(alter_parts)}"
|
|
548
|
+
|
|
549
|
+
@classmethod
|
|
550
|
+
def alter_drop(cls, table, columns):
|
|
551
|
+
drop_parts = [f"DROP COLUMN {quote(col)}" for col in columns]
|
|
552
|
+
return f"ALTER TABLE {quote(table)} {', '.join(drop_parts)}"
|
|
553
|
+
|
|
554
|
+
@classmethod
|
|
555
|
+
def alter_column_by_type(cls, table, column, value, nullable=True):
|
|
556
|
+
null_clause = "NULL" if nullable else "NOT NULL"
|
|
557
|
+
return f"ALTER TABLE {quote(table)} MODIFY COLUMN {quote(column)} {value} {null_clause}"
|
|
558
|
+
|
|
559
|
+
@classmethod
|
|
560
|
+
def alter_column_by_sql(cls, table, column, value):
|
|
561
|
+
return f"ALTER TABLE {quote(table)} MODIFY COLUMN {quote(column)} {value}"
|
|
562
|
+
|
|
563
|
+
@classmethod
|
|
564
|
+
def rename_column(cls, table, orig, new):
|
|
565
|
+
# MySQL requires the full column definition for CHANGE
|
|
566
|
+
return f"ALTER TABLE {quote(table)} CHANGE {quote(orig)} {quote(new)} /* TYPE_NEEDED */"
|
|
567
|
+
|
|
568
|
+
@classmethod
|
|
569
|
+
def rename_table(cls, table, new):
|
|
570
|
+
return f"RENAME TABLE {quote(table)} TO {quote(new)}"
|
|
571
|
+
|
|
572
|
+
@classmethod
|
|
573
|
+
def primary_keys(cls, table):
|
|
574
|
+
return f"SHOW KEYS FROM {quote(table)} WHERE Key_name = 'PRIMARY'"
|
|
575
|
+
|
|
576
|
+
@classmethod
|
|
577
|
+
def foreign_key_info(cls, table=None, column=None, schema=None):
|
|
578
|
+
sql = """
|
|
579
|
+
SELECT
|
|
580
|
+
TABLE_NAME,
|
|
581
|
+
COLUMN_NAME,
|
|
582
|
+
CONSTRAINT_NAME,
|
|
583
|
+
REFERENCED_TABLE_NAME,
|
|
584
|
+
REFERENCED_COLUMN_NAME
|
|
585
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
586
|
+
WHERE REFERENCED_TABLE_NAME IS NOT NULL
|
|
587
|
+
"""
|
|
588
|
+
if table:
|
|
589
|
+
sql += f" AND TABLE_NAME = '{table}'"
|
|
590
|
+
if column:
|
|
591
|
+
sql += f" AND COLUMN_NAME = '{column}'"
|
|
592
|
+
return sql
|
|
593
|
+
|
|
594
|
+
@classmethod
|
|
595
|
+
def create_foreign_key(cls, table, columns, key_to_table, key_to_columns, name=None, schema=None):
|
|
596
|
+
if name is None:
|
|
597
|
+
name = f"fk_{table}_{'_'.join(columns)}"
|
|
598
|
+
|
|
599
|
+
col_list = ", ".join(quote(col) for col in columns)
|
|
600
|
+
ref_col_list = ", ".join(quote(col) for col in key_to_columns)
|
|
601
|
+
|
|
602
|
+
return f"""
|
|
603
|
+
ALTER TABLE {quote(table)}
|
|
604
|
+
ADD CONSTRAINT {quote(name)}
|
|
605
|
+
FOREIGN KEY ({col_list})
|
|
606
|
+
REFERENCES {quote(key_to_table)} ({ref_col_list})
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
@classmethod
|
|
610
|
+
def drop_foreign_key(cls, table, columns, key_to_table=None, key_to_columns=None, name=None, schema=None):
|
|
611
|
+
if name is None:
|
|
612
|
+
name = f"fk_{table}_{'_'.join(columns)}"
|
|
613
|
+
|
|
614
|
+
return f"ALTER TABLE {quote(table)} DROP FOREIGN KEY {quote(name)}"
|
|
615
|
+
|
|
616
|
+
@classmethod
|
|
617
|
+
def create_index(cls, tx, table=None, columns=None, unique=False, direction=None, where=None, name=None, schema=None, trigram=None, lower=None):
|
|
618
|
+
if name is None:
|
|
619
|
+
name = f"idx_{table}_{'_'.join(columns)}"
|
|
620
|
+
|
|
621
|
+
index_type = "UNIQUE INDEX" if unique else "INDEX"
|
|
622
|
+
col_list = ", ".join(quote(col) for col in columns)
|
|
623
|
+
|
|
624
|
+
return f"CREATE {index_type} {quote(name)} ON {quote(table)} ({col_list})"
|
|
625
|
+
|
|
626
|
+
@classmethod
|
|
627
|
+
def drop_index(cls, table=None, columns=None, name=None, schema=None, trigram=None):
|
|
628
|
+
if name is None:
|
|
629
|
+
name = f"idx_{table}_{'_'.join(columns)}"
|
|
630
|
+
|
|
631
|
+
return f"DROP INDEX {quote(name)} ON {quote(table)}"
|
|
632
|
+
|
|
633
|
+
@classmethod
|
|
634
|
+
def indexes(cls, table):
|
|
635
|
+
return f"SHOW INDEX FROM {quote(table)}"
|
|
636
|
+
|
|
637
|
+
@classmethod
|
|
638
|
+
def create_savepoint(cls, sp):
|
|
639
|
+
return f"SAVEPOINT {sp}"
|
|
640
|
+
|
|
641
|
+
@classmethod
|
|
642
|
+
def release_savepoint(cls, sp):
|
|
643
|
+
return f"RELEASE SAVEPOINT {sp}"
|
|
644
|
+
|
|
645
|
+
@classmethod
|
|
646
|
+
def rollback_savepoint(cls, sp):
|
|
647
|
+
return f"ROLLBACK TO SAVEPOINT {sp}"
|
|
648
|
+
|
|
649
|
+
@classmethod
|
|
650
|
+
def create_view(cls, name, query, temp=False, silent=True):
|
|
651
|
+
if temp:
|
|
652
|
+
# MySQL doesn't support temporary views
|
|
653
|
+
temp_clause = ""
|
|
654
|
+
else:
|
|
655
|
+
temp_clause = ""
|
|
656
|
+
|
|
657
|
+
if silent:
|
|
658
|
+
return f"CREATE OR REPLACE VIEW {quote(name)} AS {query}"
|
|
659
|
+
else:
|
|
660
|
+
return f"CREATE VIEW {quote(name)} AS {query}"
|
|
661
|
+
|
|
662
|
+
@classmethod
|
|
663
|
+
def drop_view(cls, name, silent=True):
|
|
664
|
+
if silent:
|
|
665
|
+
return f"DROP VIEW IF EXISTS {quote(name)}"
|
|
666
|
+
else:
|
|
667
|
+
return f"DROP VIEW {quote(name)}"
|
|
668
|
+
|
|
669
|
+
@classmethod
|
|
670
|
+
def last_id(cls, table):
|
|
671
|
+
return "SELECT LAST_INSERT_ID()"
|
|
672
|
+
|
|
673
|
+
@classmethod
|
|
674
|
+
def current_id(cls, table):
|
|
675
|
+
return f"SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{table}'"
|
|
676
|
+
|
|
677
|
+
@classmethod
|
|
678
|
+
def set_id(cls, table, start):
|
|
679
|
+
return f"ALTER TABLE {quote(table)} AUTO_INCREMENT = {start}"
|
|
680
|
+
|
|
681
|
+
@classmethod
|
|
682
|
+
def set_sequence(cls, table, next_value):
|
|
683
|
+
return f"ALTER TABLE {quote(table)} AUTO_INCREMENT = {next_value}"
|
|
684
|
+
|
|
685
|
+
@classmethod
|
|
686
|
+
def massage_data(cls, data):
|
|
687
|
+
"""Massage data before insert/update operations."""
|
|
688
|
+
# MySQL-specific data transformations
|
|
689
|
+
return data
|
|
690
|
+
|
|
691
|
+
@classmethod
|
|
692
|
+
def alter_trigger(cls, table, state="ENABLE", name="USER"):
|
|
693
|
+
# MySQL has different trigger syntax
|
|
694
|
+
return f"-- MySQL trigger management for {table}"
|
|
695
|
+
|
|
696
|
+
@classmethod
|
|
697
|
+
def missing(cls, tx, table, list_values, column="SYS_ID", where=None):
|
|
698
|
+
"""Generate query to find missing values from a list."""
|
|
699
|
+
placeholders = ", ".join(["%s"] * len(list_values))
|
|
700
|
+
sql = f"""
|
|
701
|
+
SELECT missing_val FROM (
|
|
702
|
+
SELECT %s AS missing_val
|
|
703
|
+
{f"UNION ALL SELECT %s " * (len(list_values) - 1) if len(list_values) > 1 else ""}
|
|
704
|
+
) AS vals
|
|
705
|
+
WHERE missing_val NOT IN (
|
|
706
|
+
SELECT {quote(column)} FROM {quote(table)}
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
vals = list_values + list_values # Values appear twice in this query structure
|
|
710
|
+
|
|
711
|
+
if where:
|
|
712
|
+
where_sql, where_vals = cls._build_where(where)
|
|
713
|
+
sql += f" WHERE {where_sql}"
|
|
714
|
+
vals.extend(where_vals)
|
|
715
|
+
|
|
716
|
+
sql += ")"
|
|
717
|
+
|
|
718
|
+
return sql, vals
|