velocity-python 0.0.105__py3-none-any.whl → 0.0.155__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 +167 -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 +20 -4
- velocity/db/core/engine.py +185 -792
- velocity/db/core/result.py +36 -22
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +283 -44
- velocity/db/core/transaction.py +19 -11
- 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 +221 -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 +62 -47
- 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.105.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
- velocity_python-0.0.155.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.105.dist-info/RECORD +0 -56
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import hashlib
|
|
3
3
|
import sqlparse
|
|
4
|
-
from psycopg2 import sql
|
|
4
|
+
from psycopg2 import sql as psycopg2_sql
|
|
5
5
|
|
|
6
6
|
from velocity.db import exceptions
|
|
7
|
+
from ..base.sql import BaseSQLDialect
|
|
7
8
|
|
|
8
9
|
from .reserved import reserved_words
|
|
9
10
|
from .types import TYPES
|
|
10
|
-
from .operators import OPERATORS
|
|
11
|
+
from .operators import OPERATORS, PostgreSQLOperators
|
|
11
12
|
from ..tablehelper import TableHelper
|
|
12
13
|
from collections.abc import Mapping, Sequence
|
|
13
14
|
|
|
@@ -17,66 +18,46 @@ TableHelper.reserved = reserved_words
|
|
|
17
18
|
TableHelper.operators = OPERATORS
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def _get_table_helper(tx, table):
|
|
21
|
-
"""
|
|
22
|
-
Utility function to create a TableHelper instance.
|
|
23
|
-
Ensures consistent configuration across all SQL methods.
|
|
24
|
-
"""
|
|
25
|
-
return TableHelper(tx, table)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _validate_table_name(table):
|
|
29
|
-
"""Validate table name format."""
|
|
30
|
-
if not table or not isinstance(table, str):
|
|
31
|
-
raise ValueError("Table name must be a non-empty string")
|
|
32
|
-
# Add more validation as needed
|
|
33
|
-
return table.strip()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _handle_predicate_errors(predicates, operation="WHERE"):
|
|
37
|
-
"""Process a list of predicates with error handling."""
|
|
38
|
-
sql_parts = []
|
|
39
|
-
vals = []
|
|
40
|
-
|
|
41
|
-
for pred, val in predicates:
|
|
42
|
-
sql_parts.append(pred)
|
|
43
|
-
if val is None:
|
|
44
|
-
pass
|
|
45
|
-
elif isinstance(val, tuple):
|
|
46
|
-
vals.extend(val)
|
|
47
|
-
else:
|
|
48
|
-
vals.append(val)
|
|
49
|
-
|
|
50
|
-
return sql_parts, vals
|
|
51
|
-
|
|
52
|
-
|
|
53
21
|
system_fields = [
|
|
54
22
|
"sys_id",
|
|
55
23
|
"sys_created",
|
|
56
24
|
"sys_modified",
|
|
57
25
|
"sys_modified_by",
|
|
26
|
+
"sys_modified_row",
|
|
27
|
+
"sys_modified_count",
|
|
58
28
|
"sys_dirty",
|
|
59
29
|
"sys_table",
|
|
60
30
|
"description",
|
|
61
31
|
]
|
|
62
32
|
|
|
63
33
|
|
|
64
|
-
class SQL:
|
|
34
|
+
class SQL(BaseSQLDialect):
|
|
65
35
|
server = "PostGreSQL"
|
|
66
36
|
type_column_identifier = "data_type"
|
|
67
37
|
is_nullable = "is_nullable"
|
|
68
38
|
|
|
69
39
|
default_schema = "public"
|
|
70
40
|
|
|
71
|
-
ApplicationErrorCodes = ["22P02", "42883", "42501", "42601", "25P01", "25P02"]
|
|
41
|
+
ApplicationErrorCodes = ["22P02", "42883", "42501", "42601", "25P01", "25P02", "42804"] # Added 42804 for datatype mismatch
|
|
72
42
|
|
|
73
43
|
DatabaseMissingErrorCodes = ["3D000"]
|
|
74
44
|
TableMissingErrorCodes = ["42P01"]
|
|
75
45
|
ColumnMissingErrorCodes = ["42703"]
|
|
76
46
|
ForeignKeyMissingErrorCodes = ["42704"]
|
|
77
47
|
|
|
78
|
-
ConnectionErrorCodes = [
|
|
79
|
-
|
|
48
|
+
ConnectionErrorCodes = [
|
|
49
|
+
"08001",
|
|
50
|
+
"08S01",
|
|
51
|
+
"57P03",
|
|
52
|
+
"08006",
|
|
53
|
+
"53300",
|
|
54
|
+
"08003",
|
|
55
|
+
"08004",
|
|
56
|
+
"08P01",
|
|
57
|
+
]
|
|
58
|
+
DuplicateKeyErrorCodes = [
|
|
59
|
+
"23505"
|
|
60
|
+
] # unique_violation - no longer relying only on regex
|
|
80
61
|
RetryTransactionCodes = ["40001", "40P01", "40002"]
|
|
81
62
|
TruncationErrorCodes = ["22001"]
|
|
82
63
|
LockTimeoutErrorCodes = ["55P03"]
|
|
@@ -84,7 +65,7 @@ class SQL:
|
|
|
84
65
|
DataIntegrityErrorCodes = ["23503", "23502", "23514", "23P01", "22003"]
|
|
85
66
|
|
|
86
67
|
@classmethod
|
|
87
|
-
def get_error(
|
|
68
|
+
def get_error(cls, e):
|
|
88
69
|
error_code = getattr(e, "pgcode", None)
|
|
89
70
|
error_mesg = getattr(e, "pgerror", None)
|
|
90
71
|
return error_code, error_mesg
|
|
@@ -111,7 +92,7 @@ class SQL:
|
|
|
111
92
|
"""
|
|
112
93
|
if not table:
|
|
113
94
|
raise ValueError("Table name is required.")
|
|
114
|
-
|
|
95
|
+
|
|
115
96
|
# Validate pagination parameters
|
|
116
97
|
if start is not None and not isinstance(start, int):
|
|
117
98
|
raise ValueError("Start (OFFSET) must be an integer.")
|
|
@@ -131,7 +112,7 @@ class SQL:
|
|
|
131
112
|
vals = []
|
|
132
113
|
|
|
133
114
|
# Create table helper instance
|
|
134
|
-
th =
|
|
115
|
+
th = TableHelper(tx, table)
|
|
135
116
|
|
|
136
117
|
# Handle columns and DISTINCT before aliasing
|
|
137
118
|
if columns is None:
|
|
@@ -148,7 +129,7 @@ class SQL:
|
|
|
148
129
|
columns = [c.strip() for c in columns if c.strip()] # Remove empty columns
|
|
149
130
|
if not columns:
|
|
150
131
|
raise ValueError("No valid columns specified")
|
|
151
|
-
|
|
132
|
+
|
|
152
133
|
distinct = False
|
|
153
134
|
|
|
154
135
|
# Check for DISTINCT keyword in any column
|
|
@@ -188,7 +169,7 @@ class SQL:
|
|
|
188
169
|
new_orderby = []
|
|
189
170
|
if isinstance(orderby, str):
|
|
190
171
|
orderby = th.split_columns(orderby)
|
|
191
|
-
|
|
172
|
+
|
|
192
173
|
# Handle orderby references
|
|
193
174
|
if isinstance(orderby, Sequence):
|
|
194
175
|
for column in orderby:
|
|
@@ -200,7 +181,9 @@ class SQL:
|
|
|
200
181
|
# Validate direction
|
|
201
182
|
direction = direction.upper()
|
|
202
183
|
if direction not in ("ASC", "DESC"):
|
|
203
|
-
raise ValueError(
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Invalid ORDER BY direction: {direction}"
|
|
186
|
+
)
|
|
204
187
|
col_name = th.resolve_references(
|
|
205
188
|
col_name.strip(), options={"alias_only": True}
|
|
206
189
|
)
|
|
@@ -213,7 +196,9 @@ class SQL:
|
|
|
213
196
|
)
|
|
214
197
|
new_orderby.append(resolved_col)
|
|
215
198
|
except Exception as e:
|
|
216
|
-
raise ValueError(
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Error processing ORDER BY column '{column}': {e}"
|
|
201
|
+
)
|
|
217
202
|
|
|
218
203
|
elif isinstance(orderby, Mapping):
|
|
219
204
|
for key, val in orderby.items():
|
|
@@ -222,11 +207,13 @@ class SQL:
|
|
|
222
207
|
direction = str(val).upper()
|
|
223
208
|
if direction not in ("ASC", "DESC"):
|
|
224
209
|
raise ValueError(f"Invalid ORDER BY direction: {direction}")
|
|
225
|
-
parsed_key = th.resolve_references(
|
|
210
|
+
parsed_key = th.resolve_references(
|
|
211
|
+
key, options={"alias_only": True}
|
|
212
|
+
)
|
|
226
213
|
new_orderby.append(f"{parsed_key} {direction}")
|
|
227
214
|
except Exception as e:
|
|
228
215
|
raise ValueError(f"Error processing ORDER BY key '{key}': {e}")
|
|
229
|
-
|
|
216
|
+
|
|
230
217
|
orderby = new_orderby
|
|
231
218
|
|
|
232
219
|
# Handle groupby
|
|
@@ -256,7 +243,9 @@ class SQL:
|
|
|
256
243
|
|
|
257
244
|
# FROM clause
|
|
258
245
|
if th.foreign_keys:
|
|
259
|
-
sql_parts["FROM"].append(
|
|
246
|
+
sql_parts["FROM"].append(
|
|
247
|
+
f"{TableHelper.quote(table)} AS {TableHelper.quote(alias)}"
|
|
248
|
+
)
|
|
260
249
|
# Handle joins
|
|
261
250
|
done = []
|
|
262
251
|
for key, ref_info in th.foreign_keys.items():
|
|
@@ -276,11 +265,44 @@ class SQL:
|
|
|
276
265
|
else:
|
|
277
266
|
sql_parts["FROM"].append(TableHelper.quote(table))
|
|
278
267
|
|
|
279
|
-
# WHERE
|
|
268
|
+
# WHERE - Enhanced validation to prevent malformed SQL
|
|
280
269
|
if where:
|
|
281
270
|
if isinstance(where, str):
|
|
271
|
+
# Validate string WHERE clauses to prevent malformed SQL
|
|
272
|
+
where_stripped = where.strip()
|
|
273
|
+
if not where_stripped:
|
|
274
|
+
raise ValueError("WHERE clause cannot be empty string.")
|
|
275
|
+
# Check for boolean literals first (includes '1' and '0')
|
|
276
|
+
if where_stripped in ('True', 'False', '1', '0'):
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"Invalid WHERE clause: '{where}'. "
|
|
279
|
+
"Boolean literals alone are not valid WHERE clauses. "
|
|
280
|
+
"Use complete SQL expressions like 'sys_active = true' instead."
|
|
281
|
+
)
|
|
282
|
+
# Then check for other numeric values (excluding '1' and '0' already handled above)
|
|
283
|
+
elif where_stripped.isdigit():
|
|
284
|
+
raise ValueError(
|
|
285
|
+
f"Invalid WHERE clause: '{where}'. "
|
|
286
|
+
"Bare integers are not valid WHERE clauses. "
|
|
287
|
+
"Use a dictionary like {{'sys_id': {where_stripped}}} or "
|
|
288
|
+
f"a complete SQL expression like 'sys_id = {where_stripped}' instead."
|
|
289
|
+
)
|
|
282
290
|
sql_parts["WHERE"].append(where)
|
|
283
|
-
|
|
291
|
+
elif isinstance(where, (int, float, bool)):
|
|
292
|
+
# Handle primitive types that should be converted to proper WHERE clauses
|
|
293
|
+
suggested_fix = "{'sys_id': " + str(where) + "}" if isinstance(where, int) else "complete SQL expression"
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"Invalid WHERE clause: {where} (type: {type(where).__name__}). "
|
|
296
|
+
f"Primitive values cannot be WHERE clauses directly. "
|
|
297
|
+
f"Use a dictionary like {suggested_fix} or a complete SQL string instead. "
|
|
298
|
+
f"This error prevents PostgreSQL 'argument of WHERE must be type boolean' errors."
|
|
299
|
+
)
|
|
300
|
+
elif isinstance(where, Mapping):
|
|
301
|
+
# Convert dictionary to predicate list
|
|
302
|
+
new_where = []
|
|
303
|
+
for key, val in where.items():
|
|
304
|
+
new_where.append(th.make_predicate(key, val))
|
|
305
|
+
where = new_where
|
|
284
306
|
for pred, val in where:
|
|
285
307
|
sql_parts["WHERE"].append(pred)
|
|
286
308
|
if val is None:
|
|
@@ -289,6 +311,22 @@ class SQL:
|
|
|
289
311
|
vals.extend(val)
|
|
290
312
|
else:
|
|
291
313
|
vals.append(val)
|
|
314
|
+
else:
|
|
315
|
+
# Handle list of tuples or other iterable
|
|
316
|
+
try:
|
|
317
|
+
for pred, val in where:
|
|
318
|
+
sql_parts["WHERE"].append(pred)
|
|
319
|
+
if val is None:
|
|
320
|
+
pass
|
|
321
|
+
elif isinstance(val, tuple):
|
|
322
|
+
vals.extend(val)
|
|
323
|
+
else:
|
|
324
|
+
vals.append(val)
|
|
325
|
+
except (TypeError, ValueError) as e:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
f"Invalid WHERE clause format: {where}. "
|
|
328
|
+
"Expected dictionary, list of (predicate, value) tuples, or SQL string."
|
|
329
|
+
) from e
|
|
292
330
|
|
|
293
331
|
# GROUP BY
|
|
294
332
|
if groupby:
|
|
@@ -378,7 +416,7 @@ class SQL:
|
|
|
378
416
|
if not isinstance(data, Mapping) or not data:
|
|
379
417
|
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
380
418
|
|
|
381
|
-
th =
|
|
419
|
+
th = TableHelper(tx, table)
|
|
382
420
|
set_clauses = []
|
|
383
421
|
vals = []
|
|
384
422
|
|
|
@@ -417,17 +455,53 @@ class SQL:
|
|
|
417
455
|
for key, val in where.items():
|
|
418
456
|
new_where.append(th.make_predicate(key, val))
|
|
419
457
|
where = new_where
|
|
420
|
-
|
|
458
|
+
elif isinstance(where, str):
|
|
459
|
+
# Enhanced validation for string WHERE clauses
|
|
460
|
+
where_stripped = where.strip()
|
|
461
|
+
if not where_stripped:
|
|
462
|
+
raise ValueError("WHERE clause cannot be empty string.")
|
|
463
|
+
# Check for boolean literals first (includes '1' and '0')
|
|
464
|
+
if where_stripped in ('True', 'False', '1', '0'):
|
|
465
|
+
raise ValueError(
|
|
466
|
+
f"Invalid WHERE clause: '{where}'. "
|
|
467
|
+
"Boolean literals alone are not valid WHERE clauses. "
|
|
468
|
+
"Use complete SQL expressions like 'sys_active = true' instead."
|
|
469
|
+
)
|
|
470
|
+
# Then check for other numeric values (excluding '1' and '0' already handled above)
|
|
471
|
+
elif where_stripped.isdigit():
|
|
472
|
+
raise ValueError(
|
|
473
|
+
f"Invalid WHERE clause: '{where}'. "
|
|
474
|
+
"Bare integers are not valid WHERE clauses. "
|
|
475
|
+
f"Use a dictionary like {{'sys_id': {where_stripped}}} or "
|
|
476
|
+
f"a complete SQL expression like 'sys_id = {where_stripped}' instead."
|
|
477
|
+
)
|
|
421
478
|
where_clauses.append(where)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
479
|
+
elif isinstance(where, (int, float, bool)):
|
|
480
|
+
# Handle primitive types that should be converted to proper WHERE clauses
|
|
481
|
+
suggested_fix = "{'sys_id': " + str(where) + "}" if isinstance(where, int) else "complete SQL expression"
|
|
482
|
+
raise ValueError(
|
|
483
|
+
f"Invalid WHERE clause: {where} (type: {type(where).__name__}). "
|
|
484
|
+
f"Primitive values cannot be WHERE clauses directly. "
|
|
485
|
+
f"Use a dictionary like {suggested_fix} or a complete SQL string instead. "
|
|
486
|
+
f"This error prevents PostgreSQL 'argument of WHERE must be type boolean' errors."
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Process the where clause if it's a list of tuples
|
|
490
|
+
if not isinstance(where, str):
|
|
491
|
+
try:
|
|
492
|
+
for pred, value in where:
|
|
493
|
+
where_clauses.append(pred)
|
|
494
|
+
if value is None:
|
|
495
|
+
pass
|
|
496
|
+
elif isinstance(value, tuple):
|
|
497
|
+
vals.extend(value)
|
|
498
|
+
else:
|
|
499
|
+
vals.append(value)
|
|
500
|
+
except (TypeError, ValueError) as e:
|
|
501
|
+
raise ValueError(
|
|
502
|
+
f"Invalid WHERE clause format: {where}. "
|
|
503
|
+
"Expected dictionary, list of (predicate, value) tuples, or SQL string."
|
|
504
|
+
) from e
|
|
431
505
|
if not where_clauses:
|
|
432
506
|
raise ValueError(
|
|
433
507
|
"No WHERE clause could be constructed. Update would affect all rows."
|
|
@@ -463,7 +537,7 @@ class SQL:
|
|
|
463
537
|
# Create a temporary TableHelper instance for quoting
|
|
464
538
|
# Note: We pass None for tx since we only need quoting functionality
|
|
465
539
|
temp_helper = TableHelper(None, table)
|
|
466
|
-
|
|
540
|
+
|
|
467
541
|
keys = []
|
|
468
542
|
vals_placeholders = []
|
|
469
543
|
args = []
|
|
@@ -490,51 +564,164 @@ class SQL:
|
|
|
490
564
|
|
|
491
565
|
@classmethod
|
|
492
566
|
def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
|
|
567
|
+
if not isinstance(data, Mapping) or not data:
|
|
568
|
+
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
569
|
+
|
|
570
|
+
table_helper = TableHelper(tx, table)
|
|
571
|
+
data = dict(data) # work with a copy to avoid mutating the caller's dict
|
|
572
|
+
|
|
493
573
|
if pk is None:
|
|
494
574
|
pkeys = tx.table(table).primary_keys()
|
|
495
575
|
if not pkeys:
|
|
496
576
|
raise ValueError("Primary key required for merge.")
|
|
497
|
-
|
|
498
|
-
if
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
577
|
+
missing = [key for key in pkeys if key not in data]
|
|
578
|
+
if missing:
|
|
579
|
+
missing_cols = ", ".join(missing)
|
|
580
|
+
raise ValueError(
|
|
581
|
+
"Primary key values missing from data for merge: "
|
|
582
|
+
f"{missing_cols}. Provide pk=... or include the key values in data."
|
|
583
|
+
)
|
|
584
|
+
pk = {key: data[key] for key in pkeys}
|
|
585
|
+
else:
|
|
586
|
+
pk = dict(pk)
|
|
587
|
+
for key, value in pk.items():
|
|
588
|
+
if key in data and data[key] != value:
|
|
589
|
+
raise ValueError(
|
|
590
|
+
f"Conflicting values for primary key '{key}' between data and pk arguments."
|
|
591
|
+
)
|
|
504
592
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
full_data.update(data)
|
|
508
|
-
full_data.update(pk)
|
|
593
|
+
insert_data = dict(data)
|
|
594
|
+
insert_data.update(pk)
|
|
509
595
|
|
|
510
|
-
|
|
511
|
-
sql = [sql]
|
|
512
|
-
vals = list(vals) # Convert to a mutable list
|
|
596
|
+
update_data = {k: v for k, v in data.items() if k not in pk}
|
|
513
597
|
|
|
514
|
-
if
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if on_conflict_do_nothing:
|
|
521
|
-
sql.append("NOTHING")
|
|
522
|
-
elif on_conflict_update:
|
|
523
|
-
# Call update() with excluded=True to produce the SET clause for the upsert.
|
|
524
|
-
sql_update, vals_update = cls.update(tx, table, data, pk, excluded=True)
|
|
525
|
-
sql.append(sql_update)
|
|
526
|
-
# Use list.extend to add the update values to vals.
|
|
527
|
-
vals.extend(vals_update)
|
|
528
|
-
else:
|
|
598
|
+
if not update_data and on_conflict_update:
|
|
599
|
+
# Nothing to update, fall back to a no-op on conflict resolution.
|
|
600
|
+
on_conflict_do_nothing = True
|
|
601
|
+
on_conflict_update = False
|
|
602
|
+
|
|
603
|
+
if on_conflict_do_nothing == on_conflict_update:
|
|
529
604
|
raise Exception(
|
|
530
605
|
"Update on conflict must have one and only one option to complete on conflict."
|
|
531
606
|
)
|
|
532
607
|
|
|
608
|
+
sql, vals = cls.insert(table, insert_data)
|
|
609
|
+
sql = [sql]
|
|
610
|
+
vals = list(vals) # Convert to a mutable list
|
|
611
|
+
|
|
612
|
+
sql.append("ON CONFLICT")
|
|
613
|
+
conflict_columns = [TableHelper.quote(column) for column in pk.keys()]
|
|
614
|
+
sql.append("(")
|
|
615
|
+
sql.append(", ".join(conflict_columns))
|
|
616
|
+
sql.append(")")
|
|
617
|
+
sql.append("DO")
|
|
618
|
+
if on_conflict_do_nothing:
|
|
619
|
+
sql.append("NOTHING")
|
|
620
|
+
elif on_conflict_update:
|
|
621
|
+
sql_update, vals_update = cls.update(
|
|
622
|
+
tx, table, update_data, pk, excluded=True
|
|
623
|
+
)
|
|
624
|
+
sql.append(sql_update)
|
|
625
|
+
vals.extend(vals_update)
|
|
626
|
+
|
|
533
627
|
import sqlparse
|
|
534
628
|
|
|
535
629
|
final_sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
536
630
|
return final_sql, tuple(vals)
|
|
537
631
|
|
|
632
|
+
@classmethod
|
|
633
|
+
def insnx(cls, tx, table, data, where=None):
|
|
634
|
+
"""Insert only when the supplied predicate finds no existing row."""
|
|
635
|
+
if not table:
|
|
636
|
+
raise ValueError("Table name is required.")
|
|
637
|
+
if not isinstance(data, Mapping) or not data:
|
|
638
|
+
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
639
|
+
|
|
640
|
+
# Helper used for quoting and foreign key resolution
|
|
641
|
+
th = TableHelper(tx, table)
|
|
642
|
+
quote_helper = TableHelper(None, table)
|
|
643
|
+
|
|
644
|
+
columns_sql = []
|
|
645
|
+
select_parts = []
|
|
646
|
+
vals = []
|
|
647
|
+
|
|
648
|
+
for key, val in data.items():
|
|
649
|
+
columns_sql.append(quote_helper.quote(key.lower()))
|
|
650
|
+
if isinstance(val, str) and len(val) > 2 and val.startswith("@@") and val[2:]:
|
|
651
|
+
select_parts.append(val[2:])
|
|
652
|
+
else:
|
|
653
|
+
select_parts.append("%s")
|
|
654
|
+
vals.append(val)
|
|
655
|
+
|
|
656
|
+
if not select_parts:
|
|
657
|
+
raise ValueError("At least one column is required for insert.")
|
|
658
|
+
|
|
659
|
+
if where is None:
|
|
660
|
+
if tx is None:
|
|
661
|
+
raise ValueError(
|
|
662
|
+
"A transaction context is required when deriving WHERE from primary keys."
|
|
663
|
+
)
|
|
664
|
+
pk_cols = tx.table(table).primary_keys()
|
|
665
|
+
if not pk_cols:
|
|
666
|
+
raise ValueError("Primary key required to derive WHERE clause.")
|
|
667
|
+
missing = [pk for pk in pk_cols if pk not in data]
|
|
668
|
+
if missing:
|
|
669
|
+
raise ValueError(
|
|
670
|
+
"Missing primary key value(s) for insert condition: " + ", ".join(missing)
|
|
671
|
+
)
|
|
672
|
+
where = {pk: data[pk] for pk in pk_cols}
|
|
673
|
+
|
|
674
|
+
where_clauses = []
|
|
675
|
+
where_vals = []
|
|
676
|
+
|
|
677
|
+
if isinstance(where, Mapping):
|
|
678
|
+
compiled = []
|
|
679
|
+
for key, val in where.items():
|
|
680
|
+
compiled.append(th.make_predicate(key, val))
|
|
681
|
+
where = compiled
|
|
682
|
+
|
|
683
|
+
if isinstance(where, str):
|
|
684
|
+
where_clauses.append(where)
|
|
685
|
+
else:
|
|
686
|
+
try:
|
|
687
|
+
for predicate, value in where:
|
|
688
|
+
where_clauses.append(predicate)
|
|
689
|
+
if value is None:
|
|
690
|
+
continue
|
|
691
|
+
if isinstance(value, tuple):
|
|
692
|
+
where_vals.extend(value)
|
|
693
|
+
else:
|
|
694
|
+
where_vals.append(value)
|
|
695
|
+
except (TypeError, ValueError) as exc:
|
|
696
|
+
raise ValueError(
|
|
697
|
+
"Invalid WHERE clause format. Expected mapping, SQL string, or iterable of predicate/value pairs."
|
|
698
|
+
) from exc
|
|
699
|
+
|
|
700
|
+
vals.extend(where_vals)
|
|
701
|
+
|
|
702
|
+
exists_sql = [
|
|
703
|
+
"SELECT 1 FROM",
|
|
704
|
+
TableHelper.quote(table),
|
|
705
|
+
]
|
|
706
|
+
if where_clauses:
|
|
707
|
+
exists_sql.append("WHERE " + " AND ".join(where_clauses))
|
|
708
|
+
|
|
709
|
+
sql_parts = [
|
|
710
|
+
"INSERT INTO",
|
|
711
|
+
TableHelper.quote(table),
|
|
712
|
+
f"({','.join(columns_sql)})",
|
|
713
|
+
"SELECT",
|
|
714
|
+
", ".join(select_parts),
|
|
715
|
+
"WHERE NOT EXISTS (",
|
|
716
|
+
" ".join(exists_sql),
|
|
717
|
+
")",
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
final_sql = sqlparse.format(" ".join(sql_parts), reindent=True, keyword_case="upper")
|
|
721
|
+
return final_sql, tuple(vals)
|
|
722
|
+
|
|
723
|
+
insert_if_not_exists = insnx
|
|
724
|
+
|
|
538
725
|
@classmethod
|
|
539
726
|
def version(cls):
|
|
540
727
|
return "select version()", tuple()
|
|
@@ -614,14 +801,56 @@ class SQL:
|
|
|
614
801
|
def drop_database(cls, name):
|
|
615
802
|
return f"drop database if exists {name}", tuple()
|
|
616
803
|
|
|
804
|
+
@staticmethod
|
|
805
|
+
def _sys_modified_function_sql(schema_identifier):
|
|
806
|
+
return f"""
|
|
807
|
+
CREATE OR REPLACE FUNCTION {schema_identifier}.on_sys_modified()
|
|
808
|
+
RETURNS TRIGGER AS
|
|
809
|
+
$BODY$
|
|
810
|
+
BEGIN
|
|
811
|
+
IF (TG_OP = 'INSERT') THEN
|
|
812
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
813
|
+
NEW.sys_created := transaction_timestamp();
|
|
814
|
+
NEW.sys_modified := transaction_timestamp();
|
|
815
|
+
NEW.sys_modified_row := clock_timestamp();
|
|
816
|
+
NEW.sys_modified_count := 0;
|
|
817
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
818
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
819
|
+
NEW.sys_created := OLD.sys_created;
|
|
820
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0);
|
|
821
|
+
IF ROW(NEW.*) IS DISTINCT FROM ROW(OLD.*) THEN
|
|
822
|
+
IF OLD.sys_dirty IS TRUE AND NEW.sys_dirty IS FALSE THEN
|
|
823
|
+
NEW.sys_dirty := FALSE;
|
|
824
|
+
ELSE
|
|
825
|
+
NEW.sys_dirty := TRUE;
|
|
826
|
+
END IF;
|
|
827
|
+
NEW.sys_modified := transaction_timestamp();
|
|
828
|
+
NEW.sys_modified_row := clock_timestamp();
|
|
829
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
|
|
830
|
+
END IF;
|
|
831
|
+
END IF;
|
|
832
|
+
RETURN NEW;
|
|
833
|
+
END;
|
|
834
|
+
$BODY$
|
|
835
|
+
LANGUAGE plpgsql VOLATILE
|
|
836
|
+
COST 100;
|
|
837
|
+
"""
|
|
838
|
+
|
|
617
839
|
@classmethod
|
|
618
840
|
def create_table(cls, name, columns={}, drop=False):
|
|
619
841
|
if "." in name:
|
|
620
842
|
fqtn = TableHelper.quote(name)
|
|
621
843
|
else:
|
|
622
844
|
fqtn = f"public.{TableHelper.quote(name)}"
|
|
845
|
+
|
|
623
846
|
schema, table = fqtn.split(".")
|
|
624
|
-
|
|
847
|
+
schema_unquoted = schema.replace('"', "")
|
|
848
|
+
table_unquoted = table.replace('"', "")
|
|
849
|
+
trigger_name = (
|
|
850
|
+
f"on_update_row_{schema_unquoted}_{table_unquoted}".replace(".", "_")
|
|
851
|
+
)
|
|
852
|
+
trigger_identifier = TableHelper.quote(trigger_name)
|
|
853
|
+
schema_identifier = TableHelper.quote(schema_unquoted)
|
|
625
854
|
sql = []
|
|
626
855
|
if drop:
|
|
627
856
|
sql.append(cls.drop_table(fqtn)[0])
|
|
@@ -629,39 +858,25 @@ class SQL:
|
|
|
629
858
|
f"""
|
|
630
859
|
CREATE TABLE {fqtn} (
|
|
631
860
|
sys_id BIGSERIAL PRIMARY KEY,
|
|
632
|
-
sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
633
861
|
sys_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
634
|
-
|
|
862
|
+
sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
863
|
+
sys_modified_by TEXT NOT NULL DEFAULT 'SYSTEM',
|
|
864
|
+
sys_modified_row TIMESTAMP NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
|
865
|
+
sys_modified_count INTEGER NOT NULL DEFAULT 0,
|
|
635
866
|
sys_dirty BOOLEAN NOT NULL DEFAULT FALSE,
|
|
636
|
-
sys_table TEXT,
|
|
867
|
+
sys_table TEXT NOT NULL,
|
|
637
868
|
description TEXT
|
|
638
869
|
);
|
|
639
870
|
|
|
640
871
|
SELECT SETVAL(PG_GET_SERIAL_SEQUENCE('{fqtn}', 'sys_id'),1000,TRUE);
|
|
641
872
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
BEGIN
|
|
646
|
-
-- update sys_modified on each insert/update.
|
|
647
|
-
NEW.sys_modified := now();
|
|
648
|
-
if (TG_OP = 'INSERT') THEN
|
|
649
|
-
NEW.sys_created :=now();
|
|
650
|
-
ELSEIF (TG_OP = 'UDPATE') THEN
|
|
651
|
-
-- Do not allow sys_created to be modified.
|
|
652
|
-
NEW.sys_created := OLD.sys_created;
|
|
653
|
-
END IF;
|
|
654
|
-
-- Insert table name to row
|
|
655
|
-
NEW.sys_table := TG_TABLE_NAME;
|
|
656
|
-
RETURN NEW;
|
|
657
|
-
END;
|
|
658
|
-
$BODY$
|
|
659
|
-
LANGUAGE plpgsql VOLATILE
|
|
660
|
-
COST 100;
|
|
873
|
+
{cls._sys_modified_function_sql(schema_identifier)}
|
|
874
|
+
|
|
875
|
+
DROP TRIGGER IF EXISTS {trigger_identifier} ON {fqtn};
|
|
661
876
|
|
|
662
|
-
CREATE TRIGGER
|
|
877
|
+
CREATE TRIGGER {trigger_identifier}
|
|
663
878
|
BEFORE INSERT OR UPDATE ON {fqtn}
|
|
664
|
-
FOR EACH ROW EXECUTE PROCEDURE {
|
|
879
|
+
FOR EACH ROW EXECUTE PROCEDURE {schema_identifier}.on_sys_modified();
|
|
665
880
|
|
|
666
881
|
"""
|
|
667
882
|
)
|
|
@@ -677,6 +892,145 @@ class SQL:
|
|
|
677
892
|
sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
678
893
|
return sql, tuple()
|
|
679
894
|
|
|
895
|
+
@classmethod
|
|
896
|
+
def ensure_system_columns(cls, name, existing_columns=None, force=False):
|
|
897
|
+
"""Ensure all Velocity system columns and triggers exist for the table."""
|
|
898
|
+
existing_columns = {
|
|
899
|
+
col.lower() for col in (existing_columns or [])
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
required_columns = [
|
|
903
|
+
"sys_id",
|
|
904
|
+
"sys_created",
|
|
905
|
+
"sys_modified",
|
|
906
|
+
"sys_modified_by",
|
|
907
|
+
"sys_modified_row",
|
|
908
|
+
"sys_modified_count",
|
|
909
|
+
"sys_dirty",
|
|
910
|
+
"sys_table",
|
|
911
|
+
"description",
|
|
912
|
+
]
|
|
913
|
+
|
|
914
|
+
missing_columns = [
|
|
915
|
+
column for column in required_columns if column not in existing_columns
|
|
916
|
+
]
|
|
917
|
+
|
|
918
|
+
if not missing_columns and not force:
|
|
919
|
+
return None
|
|
920
|
+
|
|
921
|
+
if "." in name:
|
|
922
|
+
schema_name, table_name = name.split(".", 1)
|
|
923
|
+
else:
|
|
924
|
+
schema_name = cls.default_schema
|
|
925
|
+
table_name = name
|
|
926
|
+
|
|
927
|
+
schema_unquoted = schema_name.replace('"', "")
|
|
928
|
+
table_unquoted = table_name.replace('"', "")
|
|
929
|
+
|
|
930
|
+
schema_identifier = TableHelper.quote(schema_unquoted)
|
|
931
|
+
table_identifier = TableHelper.quote(table_unquoted)
|
|
932
|
+
fqtn = f"{schema_identifier}.{table_identifier}"
|
|
933
|
+
|
|
934
|
+
trigger_name = (
|
|
935
|
+
f"on_update_row_{schema_unquoted}_{table_unquoted}".replace(".", "_")
|
|
936
|
+
)
|
|
937
|
+
trigger_identifier = TableHelper.quote(trigger_name)
|
|
938
|
+
|
|
939
|
+
statements = []
|
|
940
|
+
added_columns = set()
|
|
941
|
+
columns_after = set(existing_columns)
|
|
942
|
+
|
|
943
|
+
if "sys_id" in missing_columns:
|
|
944
|
+
statements.append(
|
|
945
|
+
f"ALTER TABLE {fqtn} ADD COLUMN {TableHelper.quote('sys_id')} BIGSERIAL PRIMARY KEY;"
|
|
946
|
+
)
|
|
947
|
+
added_columns.add("sys_id")
|
|
948
|
+
columns_after.add("sys_id")
|
|
949
|
+
|
|
950
|
+
column_definitions = {
|
|
951
|
+
"sys_created": "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
952
|
+
"sys_modified": "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
953
|
+
"sys_modified_by": "TEXT NOT NULL DEFAULT 'SYSTEM'",
|
|
954
|
+
"sys_modified_row": "TIMESTAMP NOT NULL DEFAULT CLOCK_TIMESTAMP()",
|
|
955
|
+
"sys_modified_count": "INTEGER NOT NULL DEFAULT 0",
|
|
956
|
+
"sys_dirty": "BOOLEAN NOT NULL DEFAULT FALSE",
|
|
957
|
+
"sys_table": "TEXT",
|
|
958
|
+
"description": "TEXT",
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
for column, definition in column_definitions.items():
|
|
962
|
+
if column in missing_columns:
|
|
963
|
+
statements.append(
|
|
964
|
+
f"ALTER TABLE {fqtn} ADD COLUMN {TableHelper.quote(column)} {definition};"
|
|
965
|
+
)
|
|
966
|
+
added_columns.add(column)
|
|
967
|
+
columns_after.add(column)
|
|
968
|
+
|
|
969
|
+
default_map = {
|
|
970
|
+
"sys_created": "CURRENT_TIMESTAMP",
|
|
971
|
+
"sys_modified": "CURRENT_TIMESTAMP",
|
|
972
|
+
"sys_modified_by": "'SYSTEM'",
|
|
973
|
+
"sys_modified_row": "CLOCK_TIMESTAMP()",
|
|
974
|
+
"sys_modified_count": "0",
|
|
975
|
+
"sys_dirty": "FALSE",
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
for column, default_sql in default_map.items():
|
|
979
|
+
if column in columns_after and (force or column in added_columns):
|
|
980
|
+
quoted_column = TableHelper.quote(column)
|
|
981
|
+
statements.append(
|
|
982
|
+
f"UPDATE {fqtn} SET {quoted_column} = {default_sql} WHERE {quoted_column} IS NULL;"
|
|
983
|
+
)
|
|
984
|
+
statements.append(
|
|
985
|
+
f"ALTER TABLE {fqtn} ALTER COLUMN {quoted_column} SET DEFAULT {default_sql};"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
if "sys_table" in columns_after and (force or "sys_table" in added_columns):
|
|
989
|
+
quoted_column = TableHelper.quote("sys_table")
|
|
990
|
+
table_literal = table_unquoted.replace("'", "''")
|
|
991
|
+
statements.append(
|
|
992
|
+
f"UPDATE {fqtn} SET {quoted_column} = COALESCE({quoted_column}, '{table_literal}') WHERE {quoted_column} IS NULL;"
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
not_null_columns = {
|
|
996
|
+
"sys_created",
|
|
997
|
+
"sys_modified",
|
|
998
|
+
"sys_modified_by",
|
|
999
|
+
"sys_modified_row",
|
|
1000
|
+
"sys_modified_count",
|
|
1001
|
+
"sys_dirty",
|
|
1002
|
+
"sys_table",
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
for column in not_null_columns:
|
|
1006
|
+
if column in columns_after and (force or column in added_columns):
|
|
1007
|
+
statements.append(
|
|
1008
|
+
f"ALTER TABLE {fqtn} ALTER COLUMN {TableHelper.quote(column)} SET NOT NULL;"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
reset_trigger = force or bool(added_columns)
|
|
1012
|
+
|
|
1013
|
+
if reset_trigger:
|
|
1014
|
+
statements.append(
|
|
1015
|
+
f"DROP TRIGGER IF EXISTS {trigger_identifier} ON {fqtn};"
|
|
1016
|
+
)
|
|
1017
|
+
statements.append(cls._sys_modified_function_sql(schema_identifier))
|
|
1018
|
+
statements.append(
|
|
1019
|
+
f"""
|
|
1020
|
+
CREATE TRIGGER {trigger_identifier}
|
|
1021
|
+
BEFORE INSERT OR UPDATE ON {fqtn}
|
|
1022
|
+
FOR EACH ROW EXECUTE PROCEDURE {schema_identifier}.on_sys_modified();
|
|
1023
|
+
"""
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
if not statements:
|
|
1027
|
+
return None
|
|
1028
|
+
|
|
1029
|
+
sql = sqlparse.format(
|
|
1030
|
+
" ".join(statements), reindent=True, keyword_case="upper"
|
|
1031
|
+
)
|
|
1032
|
+
return sql, tuple()
|
|
1033
|
+
|
|
680
1034
|
@classmethod
|
|
681
1035
|
def drop_table(cls, name):
|
|
682
1036
|
return f"drop table if exists {TableHelper.quote(name)} cascade;", tuple()
|
|
@@ -966,7 +1320,7 @@ class SQL:
|
|
|
966
1320
|
columns = TableHelper.quote(columns)
|
|
967
1321
|
sql = ["DROP"]
|
|
968
1322
|
sql.append("INDEX IF EXISTS")
|
|
969
|
-
|
|
1323
|
+
_tablename = TableHelper.quote(table)
|
|
970
1324
|
if not name:
|
|
971
1325
|
name = re.sub(
|
|
972
1326
|
r"\([^)]*\)",
|
|
@@ -1136,9 +1490,9 @@ class SQL:
|
|
|
1136
1490
|
@classmethod
|
|
1137
1491
|
def missing(cls, tx, table, list, column="SYS_ID", where=None):
|
|
1138
1492
|
sql = [
|
|
1139
|
-
|
|
1493
|
+
"SELECT * FROM",
|
|
1140
1494
|
f"UNNEST('{{{','.join([str(x) for x in list])}}}'::int[]) id",
|
|
1141
|
-
|
|
1495
|
+
"EXCEPT ALL",
|
|
1142
1496
|
f"SELECT {column} FROM {table}",
|
|
1143
1497
|
]
|
|
1144
1498
|
vals = []
|