velocity-python 0.0.129__py3-none-any.whl → 0.0.132__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.

Potentially problematic release.


This version of velocity-python might be problematic. Click here for more details.

Files changed (40) hide show
  1. velocity/__init__.py +1 -1
  2. velocity/aws/handlers/mixins/__init__.py +16 -0
  3. velocity/aws/handlers/mixins/activity_tracker.py +142 -0
  4. velocity/aws/handlers/mixins/error_handler.py +192 -0
  5. velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
  6. velocity/aws/handlers/mixins/standard_mixin.py +73 -0
  7. velocity/db/servers/base/__init__.py +9 -0
  8. velocity/db/servers/base/initializer.py +69 -0
  9. velocity/db/servers/base/operators.py +98 -0
  10. velocity/db/servers/base/sql.py +503 -0
  11. velocity/db/servers/base/types.py +135 -0
  12. velocity/db/servers/mysql/__init__.py +64 -0
  13. velocity/db/servers/mysql/operators.py +54 -0
  14. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  15. velocity/db/servers/mysql/sql.py +569 -0
  16. velocity/db/servers/mysql/types.py +107 -0
  17. velocity/db/servers/postgres/__init__.py +40 -0
  18. velocity/db/servers/postgres/operators.py +34 -0
  19. velocity/db/servers/postgres/sql.py +4 -3
  20. velocity/db/servers/postgres/types.py +88 -2
  21. velocity/db/servers/sqlite/__init__.py +52 -0
  22. velocity/db/servers/sqlite/operators.py +52 -0
  23. velocity/db/servers/sqlite/reserved.py +20 -0
  24. velocity/db/servers/sqlite/sql.py +530 -0
  25. velocity/db/servers/sqlite/types.py +92 -0
  26. velocity/db/servers/sqlserver/__init__.py +64 -0
  27. velocity/db/servers/sqlserver/operators.py +47 -0
  28. velocity/db/servers/sqlserver/reserved.py +32 -0
  29. velocity/db/servers/sqlserver/sql.py +625 -0
  30. velocity/db/servers/sqlserver/types.py +114 -0
  31. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/METADATA +1 -1
  32. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/RECORD +35 -16
  33. velocity/db/servers/mysql.py +0 -640
  34. velocity/db/servers/sqlite.py +0 -968
  35. velocity/db/servers/sqlite_reserved.py +0 -208
  36. velocity/db/servers/sqlserver.py +0 -921
  37. velocity/db/servers/sqlserver_reserved.py +0 -314
  38. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/WHEEL +0 -0
  39. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/licenses/LICENSE +0 -0
  40. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,54 @@
1
+ from ..base.operators import BaseOperators
2
+
3
+
4
+ class MySQLOperators(BaseOperators):
5
+ """
6
+ MySQL-specific operator mappings.
7
+ """
8
+
9
+ @classmethod
10
+ def get_operators(cls):
11
+ """Returns MySQL-specific operator mappings."""
12
+ return OPERATORS
13
+
14
+ @classmethod
15
+ def supports_case_insensitive_like(cls):
16
+ """MySQL LIKE is case-insensitive by default on case-insensitive collations."""
17
+ return False # Depends on collation, but generally need to use LOWER()
18
+
19
+ @classmethod
20
+ def supports_regex(cls):
21
+ """MySQL supports REGEXP/RLIKE operators."""
22
+ return True
23
+
24
+ @classmethod
25
+ def get_regex_operators(cls):
26
+ """Returns MySQL regex operators."""
27
+ return {
28
+ "REGEXP": "REGEXP",
29
+ "RLIKE": "RLIKE",
30
+ "NOT REGEXP": "NOT REGEXP",
31
+ }
32
+
33
+
34
+ OPERATORS = {
35
+ "<>": "<>",
36
+ "!=": "<>",
37
+ "!><": "NOT BETWEEN",
38
+ ">!<": "NOT BETWEEN",
39
+ "><": "BETWEEN",
40
+ "%%": "LIKE", # MySQL doesn't have ILIKE, use LIKE with LOWER()
41
+ "!%%": "NOT LIKE",
42
+ "==": "=",
43
+ "<=": "<=",
44
+ ">=": ">=",
45
+ "<": "<",
46
+ ">": ">",
47
+ "%": "LIKE",
48
+ "!%": "NOT LIKE",
49
+ "=": "=",
50
+ "!": "<>",
51
+ "REGEXP": "REGEXP",
52
+ "!REGEXP": "NOT REGEXP",
53
+ "RLIKE": "RLIKE",
54
+ }
@@ -61,7 +61,6 @@ reserved_words = [
61
61
  "ELSEIF",
62
62
  "ENCLOSED",
63
63
  "ESCAPED",
64
- "EXCEPT",
65
64
  "EXISTS",
66
65
  "EXIT",
67
66
  "EXPLAIN",
@@ -75,8 +74,6 @@ reserved_words = [
75
74
  "FOREIGN",
76
75
  "FROM",
77
76
  "FULLTEXT",
78
- "GENERATED",
79
- "GET",
80
77
  "GRANT",
81
78
  "GROUP",
82
79
  "HAVING",
@@ -105,7 +102,6 @@ reserved_words = [
105
102
  "IS",
106
103
  "ITERATE",
107
104
  "JOIN",
108
- "JSON",
109
105
  "KEY",
110
106
  "KEYS",
111
107
  "KILL",
@@ -125,9 +121,7 @@ reserved_words = [
125
121
  "LONGTEXT",
126
122
  "LOOP",
127
123
  "LOW_PRIORITY",
128
- "MASTER_SSL_VERIFY_SERVER_CERT",
129
124
  "MATCH",
130
- "MAXVALUE",
131
125
  "MEDIUMBLOB",
132
126
  "MEDIUMINT",
133
127
  "MEDIUMTEXT",
@@ -150,7 +144,6 @@ reserved_words = [
150
144
  "OUT",
151
145
  "OUTER",
152
146
  "OUTFILE",
153
- "PARTITION",
154
147
  "PRECISION",
155
148
  "PRIMARY",
156
149
  "PROCEDURE",
@@ -167,14 +160,11 @@ reserved_words = [
167
160
  "REPEAT",
168
161
  "REPLACE",
169
162
  "REQUIRE",
170
- "RESIGNAL",
171
163
  "RESTRICT",
172
164
  "RETURN",
173
165
  "REVOKE",
174
166
  "RIGHT",
175
167
  "RLIKE",
176
- "ROW",
177
- "ROWS",
178
168
  "SCHEMA",
179
169
  "SCHEMAS",
180
170
  "SECOND_MICROSECOND",
@@ -183,7 +173,6 @@ reserved_words = [
183
173
  "SEPARATOR",
184
174
  "SET",
185
175
  "SHOW",
186
- "SIGNAL",
187
176
  "SMALLINT",
188
177
  "SPATIAL",
189
178
  "SPECIFIC",
@@ -196,7 +185,6 @@ reserved_words = [
196
185
  "SQL_SMALL_RESULT",
197
186
  "SSL",
198
187
  "STARTING",
199
- "STORED",
200
188
  "STRAIGHT_JOIN",
201
189
  "TABLE",
202
190
  "TERMINATED",
@@ -225,13 +213,13 @@ reserved_words = [
225
213
  "VARCHAR",
226
214
  "VARCHARACTER",
227
215
  "VARYING",
228
- "VIRTUAL",
229
216
  "WHEN",
230
217
  "WHERE",
231
218
  "WHILE",
232
219
  "WITH",
233
220
  "WRITE",
221
+ "X509",
234
222
  "XOR",
235
223
  "YEAR_MONTH",
236
- "ZEROFILL",
224
+ "ZEROFILL"
237
225
  ]
@@ -0,0 +1,569 @@
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
+ def quote(data):
22
+ """Quote MySQL identifiers."""
23
+ if isinstance(data, list):
24
+ return [quote(item) for item in data]
25
+ else:
26
+ parts = data.split(".")
27
+ new = []
28
+ for part in parts:
29
+ if "`" in part:
30
+ new.append(part)
31
+ elif part.upper() in reserved_words:
32
+ new.append("`" + part + "`")
33
+ elif re.findall("[/]", part):
34
+ new.append("`" + part + "`")
35
+ else:
36
+ new.append(part)
37
+ return ".".join(new)
38
+
39
+
40
+ class SQL(BaseSQLDialect):
41
+ server = "MySQL"
42
+ type_column_identifier = "DATA_TYPE"
43
+ is_nullable = "IS_NULLABLE"
44
+
45
+ default_schema = ""
46
+
47
+ ApplicationErrorCodes = []
48
+ DatabaseMissingErrorCodes = ["1049"] # ER_BAD_DB_ERROR
49
+ TableMissingErrorCodes = ["1146"] # ER_NO_SUCH_TABLE
50
+ ColumnMissingErrorCodes = ["1054"] # ER_BAD_FIELD_ERROR
51
+ ForeignKeyMissingErrorCodes = ["1005"] # ER_CANT_CREATE_TABLE
52
+ ConnectionErrorCodes = ["2002", "2003", "2006"] # Connection errors
53
+ DuplicateKeyErrorCodes = ["1062"] # ER_DUP_ENTRY
54
+ RetryTransactionCodes = ["1213"] # ER_LOCK_DEADLOCK
55
+ TruncationErrorCodes = ["1406"] # ER_DATA_TOO_LONG
56
+ LockTimeoutErrorCodes = ["1205"] # ER_LOCK_WAIT_TIMEOUT
57
+ DatabaseObjectExistsErrorCodes = ["1050"] # ER_TABLE_EXISTS_ERROR
58
+ DataIntegrityErrorCodes = ["1452", "1048", "1364"] # Foreign key, null, no default
59
+
60
+ types = TYPES
61
+
62
+ @classmethod
63
+ def get_error(cls, e):
64
+ """Extract error information from MySQL exception."""
65
+ error_code = getattr(e, "errno", None)
66
+ error_msg = getattr(e, "msg", None)
67
+ return error_code, error_msg
68
+
69
+ @classmethod
70
+ def select(
71
+ cls,
72
+ tx,
73
+ columns=None,
74
+ table=None,
75
+ where=None,
76
+ orderby=None,
77
+ groupby=None,
78
+ having=None,
79
+ start=None,
80
+ qty=None,
81
+ lock=None,
82
+ skip_locked=None,
83
+ ):
84
+ """Generate a MySQL SELECT statement."""
85
+ if not table:
86
+ raise ValueError("Table name is required")
87
+
88
+ sql_parts = []
89
+ vals = []
90
+
91
+ # SELECT clause
92
+ if columns is None:
93
+ columns = ["*"]
94
+ elif isinstance(columns, str):
95
+ columns = [columns]
96
+
97
+ sql_parts.append("SELECT")
98
+ sql_parts.append(", ".join(columns))
99
+
100
+ # FROM clause
101
+ sql_parts.append("FROM")
102
+ sql_parts.append(quote(table))
103
+
104
+ # WHERE clause
105
+ if where:
106
+ where_sql, where_vals = cls._build_where(where)
107
+ sql_parts.append("WHERE")
108
+ sql_parts.append(where_sql)
109
+ vals.extend(where_vals)
110
+
111
+ # GROUP BY clause
112
+ if groupby:
113
+ if isinstance(groupby, str):
114
+ groupby = [groupby]
115
+ sql_parts.append("GROUP BY")
116
+ sql_parts.append(", ".join(quote(col) for col in groupby))
117
+
118
+ # HAVING clause
119
+ if having:
120
+ having_sql, having_vals = cls._build_where(having)
121
+ sql_parts.append("HAVING")
122
+ sql_parts.append(having_sql)
123
+ vals.extend(having_vals)
124
+
125
+ # ORDER BY clause
126
+ if orderby:
127
+ if isinstance(orderby, str):
128
+ orderby = [orderby]
129
+ elif isinstance(orderby, dict):
130
+ orderby_list = []
131
+ for col, direction in orderby.items():
132
+ orderby_list.append(f"{quote(col)} {direction.upper()}")
133
+ orderby = orderby_list
134
+ sql_parts.append("ORDER BY")
135
+ sql_parts.append(", ".join(orderby))
136
+
137
+ # LIMIT clause (MySQL uses LIMIT instead of OFFSET/FETCH)
138
+ if start is not None and qty is not None:
139
+ sql_parts.append(f"LIMIT {start}, {qty}")
140
+ elif qty is not None:
141
+ sql_parts.append(f"LIMIT {qty}")
142
+
143
+ # FOR UPDATE (lock)
144
+ if lock:
145
+ sql_parts.append("FOR UPDATE")
146
+ if skip_locked:
147
+ sql_parts.append("SKIP LOCKED")
148
+
149
+ return " ".join(sql_parts), vals
150
+
151
+ @classmethod
152
+ def _build_where(cls, where):
153
+ """Build WHERE clause for MySQL."""
154
+ if isinstance(where, str):
155
+ return where, []
156
+
157
+ if isinstance(where, dict):
158
+ where = list(where.items())
159
+
160
+ if not isinstance(where, (list, tuple)):
161
+ raise ValueError("WHERE clause must be string, dict, or list")
162
+
163
+ conditions = []
164
+ vals = []
165
+
166
+ for key, val in where:
167
+ if val is None:
168
+ if "!" in key:
169
+ key = key.replace("!", "")
170
+ conditions.append(f"{quote(key)} IS NOT NULL")
171
+ else:
172
+ conditions.append(f"{quote(key)} IS NULL")
173
+ elif isinstance(val, (list, tuple)):
174
+ if "!" in key:
175
+ key = key.replace("!", "")
176
+ conditions.append(f"{quote(key)} NOT IN ({', '.join(['%s'] * len(val))})")
177
+ else:
178
+ conditions.append(f"{quote(key)} IN ({', '.join(['%s'] * len(val))})")
179
+ vals.extend(val)
180
+ else:
181
+ # Handle operators
182
+ op = "="
183
+ if "<>" in key:
184
+ key = key.replace("<>", "")
185
+ op = "<>"
186
+ elif "!=" in key:
187
+ key = key.replace("!=", "")
188
+ op = "<>"
189
+ elif "%%" in key:
190
+ key = key.replace("%%", "")
191
+ op = "LIKE"
192
+ elif "%" in key:
193
+ key = key.replace("%", "")
194
+ op = "LIKE"
195
+ elif "!" in key:
196
+ key = key.replace("!", "")
197
+ op = "<>"
198
+
199
+ conditions.append(f"{quote(key)} {op} %s")
200
+ vals.append(val)
201
+
202
+ return " AND ".join(conditions), vals
203
+
204
+ @classmethod
205
+ def insert(cls, table, data):
206
+ """Generate an INSERT statement for MySQL."""
207
+ if not data:
208
+ raise ValueError("Data cannot be empty")
209
+
210
+ columns = list(data.keys())
211
+ values = list(data.values())
212
+
213
+ sql_parts = [
214
+ "INSERT INTO",
215
+ quote(table),
216
+ f"({', '.join(quote(col) for col in columns)})",
217
+ "VALUES",
218
+ f"({', '.join(['%s'] * len(values))})"
219
+ ]
220
+
221
+ return " ".join(sql_parts), values
222
+
223
+ @classmethod
224
+ def update(cls, tx, table, data, where=None, pk=None, excluded=False):
225
+ """Generate an UPDATE statement for MySQL."""
226
+ if not data:
227
+ raise ValueError("Data cannot be empty")
228
+
229
+ if not where and not pk:
230
+ raise ValueError("Either WHERE clause or primary key must be provided")
231
+
232
+ # Build SET clause
233
+ set_clauses = []
234
+ vals = []
235
+
236
+ for col, val in data.items():
237
+ if excluded:
238
+ # For ON DUPLICATE KEY UPDATE
239
+ set_clauses.append(f"{quote(col)} = VALUES({quote(col)})")
240
+ else:
241
+ set_clauses.append(f"{quote(col)} = %s")
242
+ vals.append(val)
243
+
244
+ # Build WHERE clause
245
+ if pk:
246
+ if where:
247
+ # Merge pk into where
248
+ if isinstance(where, dict):
249
+ where.update(pk)
250
+ else:
251
+ # Convert to dict for merging
252
+ where_dict = dict(where) if isinstance(where, (list, tuple)) else {}
253
+ where_dict.update(pk)
254
+ where = where_dict
255
+ else:
256
+ where = pk
257
+
258
+ where_sql, where_vals = cls._build_where(where) if where else ("", [])
259
+
260
+ sql_parts = [
261
+ "UPDATE",
262
+ quote(table),
263
+ "SET",
264
+ ", ".join(set_clauses)
265
+ ]
266
+
267
+ if where_sql:
268
+ sql_parts.extend(["WHERE", where_sql])
269
+ vals.extend(where_vals)
270
+
271
+ return " ".join(sql_parts), vals
272
+
273
+ @classmethod
274
+ def delete(cls, tx, table, where):
275
+ """Generate a DELETE statement for MySQL."""
276
+ if not where:
277
+ raise ValueError("WHERE clause is required for DELETE")
278
+
279
+ where_sql, where_vals = cls._build_where(where)
280
+
281
+ sql_parts = [
282
+ "DELETE FROM",
283
+ quote(table),
284
+ "WHERE",
285
+ where_sql
286
+ ]
287
+
288
+ return " ".join(sql_parts), where_vals
289
+
290
+ @classmethod
291
+ def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
292
+ """Generate an INSERT ... ON DUPLICATE KEY UPDATE statement for MySQL."""
293
+ # First, create the INSERT part
294
+ insert_sql, insert_vals = cls.insert(table, data)
295
+
296
+ if on_conflict_do_nothing:
297
+ # MySQL: INSERT IGNORE
298
+ insert_sql = insert_sql.replace("INSERT INTO", "INSERT IGNORE INTO")
299
+ return insert_sql, insert_vals
300
+ elif on_conflict_update:
301
+ # MySQL: INSERT ... ON DUPLICATE KEY UPDATE
302
+ update_clauses = []
303
+ for col in data.keys():
304
+ if col not in pk: # Don't update primary key columns
305
+ update_clauses.append(f"{quote(col)} = VALUES({quote(col)})")
306
+
307
+ if update_clauses:
308
+ insert_sql += f" ON DUPLICATE KEY UPDATE {', '.join(update_clauses)}"
309
+
310
+ return insert_sql, insert_vals
311
+ else:
312
+ return insert_sql, insert_vals
313
+
314
+ # Metadata queries
315
+ @classmethod
316
+ def version(cls):
317
+ return "SELECT VERSION()"
318
+
319
+ @classmethod
320
+ def timestamp(cls):
321
+ return "SELECT NOW()"
322
+
323
+ @classmethod
324
+ def user(cls):
325
+ return "SELECT USER()"
326
+
327
+ @classmethod
328
+ def databases(cls):
329
+ return "SHOW DATABASES"
330
+
331
+ @classmethod
332
+ def schemas(cls):
333
+ return "SHOW DATABASES" # MySQL databases are schemas
334
+
335
+ @classmethod
336
+ def current_schema(cls):
337
+ return "SELECT DATABASE()"
338
+
339
+ @classmethod
340
+ def current_database(cls):
341
+ return "SELECT DATABASE()"
342
+
343
+ @classmethod
344
+ def tables(cls, system=False):
345
+ if system:
346
+ return "SHOW TABLES"
347
+ else:
348
+ return "SHOW TABLES"
349
+
350
+ @classmethod
351
+ def views(cls, system=False):
352
+ return "SHOW FULL TABLES WHERE Table_type = 'VIEW'"
353
+
354
+ @classmethod
355
+ def create_database(cls, name):
356
+ return f"CREATE DATABASE {quote(name)}"
357
+
358
+ @classmethod
359
+ def drop_database(cls, name):
360
+ return f"DROP DATABASE {quote(name)}"
361
+
362
+ @classmethod
363
+ def create_table(cls, name, columns=None, drop=False):
364
+ if drop:
365
+ drop_sql = f"DROP TABLE IF EXISTS {quote(name)}"
366
+ return drop_sql
367
+
368
+ # Basic CREATE TABLE - would need more implementation
369
+ return f"CREATE TABLE {quote(name)} (id INT PRIMARY KEY AUTO_INCREMENT)"
370
+
371
+ @classmethod
372
+ def drop_table(cls, name):
373
+ return f"DROP TABLE {quote(name)}"
374
+
375
+ @classmethod
376
+ def truncate(cls, table):
377
+ return f"TRUNCATE TABLE {quote(table)}"
378
+
379
+ @classmethod
380
+ def columns(cls, name):
381
+ return f"SHOW COLUMNS FROM {quote(name)}"
382
+
383
+ @classmethod
384
+ def column_info(cls, table, name):
385
+ return f"SHOW COLUMNS FROM {quote(table)} LIKE '{name}'"
386
+
387
+ @classmethod
388
+ def drop_column(cls, table, name, cascade=True):
389
+ return f"ALTER TABLE {quote(table)} DROP COLUMN {quote(name)}"
390
+
391
+ @classmethod
392
+ def alter_add(cls, table, columns, null_allowed=True):
393
+ alter_parts = []
394
+ for col, col_type in columns.items():
395
+ null_clause = "NULL" if null_allowed else "NOT NULL"
396
+ alter_parts.append(f"ADD COLUMN {quote(col)} {col_type} {null_clause}")
397
+
398
+ return f"ALTER TABLE {quote(table)} {', '.join(alter_parts)}"
399
+
400
+ @classmethod
401
+ def alter_drop(cls, table, columns):
402
+ drop_parts = [f"DROP COLUMN {quote(col)}" for col in columns]
403
+ return f"ALTER TABLE {quote(table)} {', '.join(drop_parts)}"
404
+
405
+ @classmethod
406
+ def alter_column_by_type(cls, table, column, value, nullable=True):
407
+ null_clause = "NULL" if nullable else "NOT NULL"
408
+ return f"ALTER TABLE {quote(table)} MODIFY COLUMN {quote(column)} {value} {null_clause}"
409
+
410
+ @classmethod
411
+ def alter_column_by_sql(cls, table, column, value):
412
+ return f"ALTER TABLE {quote(table)} MODIFY COLUMN {quote(column)} {value}"
413
+
414
+ @classmethod
415
+ def rename_column(cls, table, orig, new):
416
+ # MySQL requires the full column definition for CHANGE
417
+ return f"ALTER TABLE {quote(table)} CHANGE {quote(orig)} {quote(new)} /* TYPE_NEEDED */"
418
+
419
+ @classmethod
420
+ def rename_table(cls, table, new):
421
+ return f"RENAME TABLE {quote(table)} TO {quote(new)}"
422
+
423
+ @classmethod
424
+ def primary_keys(cls, table):
425
+ return f"SHOW KEYS FROM {quote(table)} WHERE Key_name = 'PRIMARY'"
426
+
427
+ @classmethod
428
+ def foreign_key_info(cls, table=None, column=None, schema=None):
429
+ sql = """
430
+ SELECT
431
+ TABLE_NAME,
432
+ COLUMN_NAME,
433
+ CONSTRAINT_NAME,
434
+ REFERENCED_TABLE_NAME,
435
+ REFERENCED_COLUMN_NAME
436
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
437
+ WHERE REFERENCED_TABLE_NAME IS NOT NULL
438
+ """
439
+ if table:
440
+ sql += f" AND TABLE_NAME = '{table}'"
441
+ if column:
442
+ sql += f" AND COLUMN_NAME = '{column}'"
443
+ return sql
444
+
445
+ @classmethod
446
+ def create_foreign_key(cls, table, columns, key_to_table, key_to_columns, name=None, schema=None):
447
+ if name is None:
448
+ name = f"fk_{table}_{'_'.join(columns)}"
449
+
450
+ col_list = ", ".join(quote(col) for col in columns)
451
+ ref_col_list = ", ".join(quote(col) for col in key_to_columns)
452
+
453
+ return f"""
454
+ ALTER TABLE {quote(table)}
455
+ ADD CONSTRAINT {quote(name)}
456
+ FOREIGN KEY ({col_list})
457
+ REFERENCES {quote(key_to_table)} ({ref_col_list})
458
+ """
459
+
460
+ @classmethod
461
+ def drop_foreign_key(cls, table, columns, key_to_table=None, key_to_columns=None, name=None, schema=None):
462
+ if name is None:
463
+ name = f"fk_{table}_{'_'.join(columns)}"
464
+
465
+ return f"ALTER TABLE {quote(table)} DROP FOREIGN KEY {quote(name)}"
466
+
467
+ @classmethod
468
+ def create_index(cls, tx, table=None, columns=None, unique=False, direction=None, where=None, name=None, schema=None, trigram=None, lower=None):
469
+ if name is None:
470
+ name = f"idx_{table}_{'_'.join(columns)}"
471
+
472
+ index_type = "UNIQUE INDEX" if unique else "INDEX"
473
+ col_list = ", ".join(quote(col) for col in columns)
474
+
475
+ return f"CREATE {index_type} {quote(name)} ON {quote(table)} ({col_list})"
476
+
477
+ @classmethod
478
+ def drop_index(cls, table=None, columns=None, name=None, schema=None, trigram=None):
479
+ if name is None:
480
+ name = f"idx_{table}_{'_'.join(columns)}"
481
+
482
+ return f"DROP INDEX {quote(name)} ON {quote(table)}"
483
+
484
+ @classmethod
485
+ def indexes(cls, table):
486
+ return f"SHOW INDEX FROM {quote(table)}"
487
+
488
+ @classmethod
489
+ def create_savepoint(cls, sp):
490
+ return f"SAVEPOINT {sp}"
491
+
492
+ @classmethod
493
+ def release_savepoint(cls, sp):
494
+ return f"RELEASE SAVEPOINT {sp}"
495
+
496
+ @classmethod
497
+ def rollback_savepoint(cls, sp):
498
+ return f"ROLLBACK TO SAVEPOINT {sp}"
499
+
500
+ @classmethod
501
+ def create_view(cls, name, query, temp=False, silent=True):
502
+ if temp:
503
+ # MySQL doesn't support temporary views
504
+ temp_clause = ""
505
+ else:
506
+ temp_clause = ""
507
+
508
+ if silent:
509
+ return f"CREATE OR REPLACE VIEW {quote(name)} AS {query}"
510
+ else:
511
+ return f"CREATE VIEW {quote(name)} AS {query}"
512
+
513
+ @classmethod
514
+ def drop_view(cls, name, silent=True):
515
+ if silent:
516
+ return f"DROP VIEW IF EXISTS {quote(name)}"
517
+ else:
518
+ return f"DROP VIEW {quote(name)}"
519
+
520
+ @classmethod
521
+ def last_id(cls, table):
522
+ return "SELECT LAST_INSERT_ID()"
523
+
524
+ @classmethod
525
+ def current_id(cls, table):
526
+ return f"SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{table}'"
527
+
528
+ @classmethod
529
+ def set_id(cls, table, start):
530
+ return f"ALTER TABLE {quote(table)} AUTO_INCREMENT = {start}"
531
+
532
+ @classmethod
533
+ def set_sequence(cls, table, next_value):
534
+ return f"ALTER TABLE {quote(table)} AUTO_INCREMENT = {next_value}"
535
+
536
+ @classmethod
537
+ def massage_data(cls, data):
538
+ """Massage data before insert/update operations."""
539
+ # MySQL-specific data transformations
540
+ return data
541
+
542
+ @classmethod
543
+ def alter_trigger(cls, table, state="ENABLE", name="USER"):
544
+ # MySQL has different trigger syntax
545
+ return f"-- MySQL trigger management for {table}"
546
+
547
+ @classmethod
548
+ def missing(cls, tx, table, list_values, column="SYS_ID", where=None):
549
+ """Generate query to find missing values from a list."""
550
+ placeholders = ", ".join(["%s"] * len(list_values))
551
+ sql = f"""
552
+ SELECT missing_val FROM (
553
+ SELECT %s AS missing_val
554
+ {f"UNION ALL SELECT %s " * (len(list_values) - 1) if len(list_values) > 1 else ""}
555
+ ) AS vals
556
+ WHERE missing_val NOT IN (
557
+ SELECT {quote(column)} FROM {quote(table)}
558
+ """
559
+
560
+ vals = list_values + list_values # Values appear twice in this query structure
561
+
562
+ if where:
563
+ where_sql, where_vals = cls._build_where(where)
564
+ sql += f" WHERE {where_sql}"
565
+ vals.extend(where_vals)
566
+
567
+ sql += ")"
568
+
569
+ return sql, vals