velocity-python 0.0.139__tar.gz → 0.0.141__tar.gz
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_python-0.0.139 → velocity_python-0.0.141}/PKG-INFO +1 -1
- {velocity_python-0.0.139 → velocity_python-0.0.141}/pyproject.toml +1 -1
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/__init__.py +1 -1
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/mixins/activity_tracker.py +53 -14
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/table.py +22 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/mysql/sql.py +132 -5
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/postgres/sql.py +81 -15
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlite/sql.py +127 -4
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlserver/sql.py +158 -4
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.0.139 → velocity_python-0.0.141}/LICENSE +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/README.md +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/setup.cfg +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.139 → velocity_python-0.0.141}/tests/test_where_clause_validation.py +0 -0
|
@@ -9,33 +9,34 @@ import copy
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import time
|
|
12
|
-
from abc import ABC
|
|
13
|
-
from
|
|
12
|
+
from abc import ABC
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
from typing import Dict, Any
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ActivityTracker(ABC):
|
|
17
18
|
"""
|
|
18
19
|
Mixin class providing standardized activity tracking for Lambda handlers.
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
Tracks API calls to the aws_api_activity table with consistent data structure
|
|
21
22
|
and automatic duration calculation.
|
|
22
23
|
"""
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
def __init__(self, *args, **kwargs):
|
|
25
26
|
super().__init__(*args, **kwargs)
|
|
26
27
|
self.activity_log_key = None
|
|
27
28
|
self.start_time = None
|
|
28
29
|
self.end_time = None
|
|
29
30
|
self.activity_data = {}
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
def track_activity_start(self, tx, context):
|
|
32
33
|
"""Start tracking activity for the current request"""
|
|
33
34
|
self.start_time = time.time()
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
# Gather common activity data
|
|
36
37
|
postdata = context.postdata()
|
|
37
38
|
payload = context.payload()
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
self.activity_data = {
|
|
40
41
|
"action": context.action(),
|
|
41
42
|
"args": json.dumps(context.args()),
|
|
@@ -45,21 +46,34 @@ class ActivityTracker(ABC):
|
|
|
45
46
|
"user_branch": os.environ.get("USER_BRANCH", "Unknown"),
|
|
46
47
|
"start_timestamp": self.start_time,
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
# Add user information if available
|
|
50
51
|
user_info = self._extract_user_info(payload)
|
|
51
52
|
if user_info:
|
|
52
53
|
self.activity_data.update(user_info)
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
# Add session information
|
|
55
56
|
session_data = context.session()
|
|
56
57
|
if session_data:
|
|
57
|
-
self.activity_data.update(
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
self.activity_data.update(self._sanitize_session_data(session_data))
|
|
59
|
+
|
|
60
|
+
# Ensure all values are serializable before persisting
|
|
61
|
+
self.activity_data = {
|
|
62
|
+
key: self._normalize_activity_value(value)
|
|
63
|
+
for key, value in self.activity_data.items()
|
|
64
|
+
if value is not None
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
# Create the activity record
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
try:
|
|
69
|
+
self.activity_log_key = tx.table("aws_api_activity").new(self.activity_data).pk
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
context.log(
|
|
72
|
+
f"ActivityTracker.track_activity_start failed: {exc}; keys={list(self.activity_data.keys())}",
|
|
73
|
+
"ActivityTracker.track_activity_start",
|
|
74
|
+
)
|
|
75
|
+
raise
|
|
76
|
+
|
|
63
77
|
return self.activity_log_key
|
|
64
78
|
|
|
65
79
|
def track_activity_success(self, tx, context):
|
|
@@ -140,3 +154,28 @@ class ActivityTracker(ABC):
|
|
|
140
154
|
pass
|
|
141
155
|
|
|
142
156
|
return user_info
|
|
157
|
+
|
|
158
|
+
def _sanitize_session_data(self, session: Dict[str, Any]) -> Dict[str, Any]:
|
|
159
|
+
"""Remove sensitive session keys and normalize value types"""
|
|
160
|
+
sanitized = {}
|
|
161
|
+
|
|
162
|
+
for key, value in session.items():
|
|
163
|
+
if key == "cognito_user":
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
sanitized[key] = self._normalize_activity_value(value)
|
|
167
|
+
|
|
168
|
+
return sanitized
|
|
169
|
+
|
|
170
|
+
def _normalize_activity_value(self, value: Any) -> Any:
|
|
171
|
+
"""Convert activity data values into types acceptable by psycopg2"""
|
|
172
|
+
if isinstance(value, (dict, list, tuple, set)):
|
|
173
|
+
try:
|
|
174
|
+
return json.dumps(value)
|
|
175
|
+
except (TypeError, ValueError):
|
|
176
|
+
return str(value)
|
|
177
|
+
if isinstance(value, (datetime, date)):
|
|
178
|
+
return value.isoformat()
|
|
179
|
+
if isinstance(value, (bytes, bytearray)):
|
|
180
|
+
return value.decode("utf-8", errors="ignore")
|
|
181
|
+
return value
|
|
@@ -163,6 +163,28 @@ class Table:
|
|
|
163
163
|
return self.name in [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
|
|
164
164
|
return self.name in [x[1] for x in result.as_tuple()]
|
|
165
165
|
|
|
166
|
+
def ensure_sys_modified_count(self, **kwds):
|
|
167
|
+
"""
|
|
168
|
+
Ensure the sys_modified_count column and trigger exist for this table.
|
|
169
|
+
|
|
170
|
+
Returns early when the column is already present unless `force=True` is provided.
|
|
171
|
+
"""
|
|
172
|
+
force = kwds.get("force", False)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
columns = [col.lower() for col in self.sys_columns()]
|
|
176
|
+
except Exception:
|
|
177
|
+
columns = []
|
|
178
|
+
|
|
179
|
+
has_column = "sys_modified_count" in columns
|
|
180
|
+
if has_column and not force:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
sql, vals = self.sql.ensure_sys_modified_count(self.name, has_column=has_column)
|
|
184
|
+
if kwds.get("sql_only", False):
|
|
185
|
+
return sql, vals
|
|
186
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
187
|
+
|
|
166
188
|
def column(self, name):
|
|
167
189
|
"""
|
|
168
190
|
Returns a Column object for the given column name.
|
|
@@ -18,6 +18,18 @@ TableHelper.reserved = reserved_words
|
|
|
18
18
|
TableHelper.operators = OPERATORS
|
|
19
19
|
|
|
20
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
|
+
|
|
21
33
|
def quote(data):
|
|
22
34
|
"""Quote MySQL identifiers."""
|
|
23
35
|
if isinstance(data, list):
|
|
@@ -361,12 +373,127 @@ class SQL(BaseSQLDialect):
|
|
|
361
373
|
|
|
362
374
|
@classmethod
|
|
363
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 = []
|
|
364
386
|
if drop:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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_sys_modified_count(cls, name):
|
|
454
|
+
"""Ensure sys_modified_count column and associated triggers exist for the table."""
|
|
455
|
+
table_identifier = quote(name)
|
|
456
|
+
base_name = name.split(".")[-1].replace("`", "")
|
|
457
|
+
base_name_sql = base_name.replace("'", "''")
|
|
458
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
|
|
459
|
+
|
|
460
|
+
statements = [
|
|
461
|
+
f"ALTER TABLE {table_identifier} ADD COLUMN IF NOT EXISTS `sys_modified_count` INT NOT NULL DEFAULT 0;",
|
|
462
|
+
f"UPDATE {table_identifier} SET `sys_modified_count` = 0 WHERE `sys_modified_count` IS NULL;",
|
|
463
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_bi;",
|
|
464
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_bu;",
|
|
465
|
+
f"""
|
|
466
|
+
CREATE TRIGGER {trigger_prefix}_bi
|
|
467
|
+
BEFORE INSERT ON {table_identifier}
|
|
468
|
+
FOR EACH ROW
|
|
469
|
+
BEGIN
|
|
470
|
+
SET NEW.sys_created = COALESCE(NEW.sys_created, NOW());
|
|
471
|
+
SET NEW.sys_modified = NOW();
|
|
472
|
+
SET NEW.sys_modified_count = 0;
|
|
473
|
+
SET NEW.sys_dirty = IFNULL(NEW.sys_dirty, 0);
|
|
474
|
+
SET NEW.sys_table = '{base_name_sql}';
|
|
475
|
+
END;
|
|
476
|
+
""".strip(),
|
|
477
|
+
f"""
|
|
478
|
+
CREATE TRIGGER {trigger_prefix}_bu
|
|
479
|
+
BEFORE UPDATE ON {table_identifier}
|
|
480
|
+
FOR EACH ROW
|
|
481
|
+
BEGIN
|
|
482
|
+
IF OLD.sys_dirty = TRUE AND NEW.sys_dirty = FALSE THEN
|
|
483
|
+
SET NEW.sys_dirty = 0;
|
|
484
|
+
SET NEW.sys_modified_count = IFNULL(OLD.sys_modified_count, 0);
|
|
485
|
+
ELSE
|
|
486
|
+
SET NEW.sys_dirty = 1;
|
|
487
|
+
SET NEW.sys_modified_count = IFNULL(OLD.sys_modified_count, 0) + 1;
|
|
488
|
+
END IF;
|
|
489
|
+
SET NEW.sys_created = OLD.sys_created;
|
|
490
|
+
SET NEW.sys_modified = NOW();
|
|
491
|
+
SET NEW.sys_table = '{base_name_sql}';
|
|
492
|
+
END;
|
|
493
|
+
""".strip(),
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
return "\n".join(statements), tuple()
|
|
370
497
|
|
|
371
498
|
@classmethod
|
|
372
499
|
def drop_table(cls, name):
|
|
@@ -734,11 +734,12 @@ class SQL(BaseSQLDialect):
|
|
|
734
734
|
f"""
|
|
735
735
|
CREATE TABLE {fqtn} (
|
|
736
736
|
sys_id BIGSERIAL PRIMARY KEY,
|
|
737
|
-
sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
738
737
|
sys_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
739
|
-
|
|
738
|
+
sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
739
|
+
sys_modified_by TEXT NOT NULL DEFAULT 'SYSTEM',
|
|
740
|
+
sys_modified_count INTEGER NOT NULL DEFAULT 0,
|
|
740
741
|
sys_dirty BOOLEAN NOT NULL DEFAULT FALSE,
|
|
741
|
-
sys_table TEXT,
|
|
742
|
+
sys_table TEXT NOT NULL,
|
|
742
743
|
description TEXT
|
|
743
744
|
);
|
|
744
745
|
|
|
@@ -747,19 +748,29 @@ class SQL(BaseSQLDialect):
|
|
|
747
748
|
CREATE OR REPLACE FUNCTION {schema}.on_sys_modified()
|
|
748
749
|
RETURNS TRIGGER AS
|
|
749
750
|
$BODY$
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
751
|
+
BEGIN
|
|
752
|
+
IF (TG_OP = 'INSERT') THEN
|
|
753
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
754
|
+
NEW.sys_created := clock_timestamp();
|
|
755
|
+
NEW.sys_modified := clock_timestamp();
|
|
756
|
+
NEW.sys_modified_count := 0;
|
|
757
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
758
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
759
|
+
NEW.sys_created := OLD.sys_created;
|
|
760
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0);
|
|
761
|
+
IF ROW(NEW.*) IS DISTINCT FROM ROW(OLD.*) THEN
|
|
762
|
+
IF OLD.sys_dirty IS TRUE AND NEW.sys_dirty IS FALSE THEN
|
|
763
|
+
NEW.sys_dirty := FALSE;
|
|
764
|
+
ELSE
|
|
765
|
+
NEW.sys_dirty := TRUE;
|
|
758
766
|
END IF;
|
|
759
|
-
|
|
760
|
-
NEW.
|
|
761
|
-
|
|
762
|
-
|
|
767
|
+
NEW.sys_modified := clock_timestamp();
|
|
768
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
|
|
769
|
+
END IF;
|
|
770
|
+
END IF;
|
|
771
|
+
|
|
772
|
+
RETURN NEW;
|
|
773
|
+
END;
|
|
763
774
|
$BODY$
|
|
764
775
|
LANGUAGE plpgsql VOLATILE
|
|
765
776
|
COST 100;
|
|
@@ -782,6 +793,61 @@ class SQL(BaseSQLDialect):
|
|
|
782
793
|
sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
783
794
|
return sql, tuple()
|
|
784
795
|
|
|
796
|
+
@classmethod
|
|
797
|
+
def ensure_sys_modified_count(cls, name):
|
|
798
|
+
"""Return SQL to backfill sys_modified_count and refresh the on_sys_modified trigger."""
|
|
799
|
+
if "." in name:
|
|
800
|
+
fqtn = TableHelper.quote(name)
|
|
801
|
+
else:
|
|
802
|
+
fqtn = f"public.{TableHelper.quote(name)}"
|
|
803
|
+
schema, _ = fqtn.split(".")
|
|
804
|
+
trigger_name = f"on_update_row_{fqtn.replace('.', '_')}"
|
|
805
|
+
column_name = TableHelper.quote("sys_modified_count")
|
|
806
|
+
|
|
807
|
+
sql = [
|
|
808
|
+
f"ALTER TABLE {fqtn} ADD COLUMN {column_name} INTEGER NOT NULL DEFAULT 0;",
|
|
809
|
+
f"UPDATE {fqtn} SET {column_name} = 0 WHERE {column_name} IS NULL;",
|
|
810
|
+
f"""
|
|
811
|
+
CREATE OR REPLACE FUNCTION {schema}.on_sys_modified()
|
|
812
|
+
RETURNS TRIGGER AS
|
|
813
|
+
$BODY$
|
|
814
|
+
BEGIN
|
|
815
|
+
IF (TG_OP = 'INSERT') THEN
|
|
816
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
817
|
+
NEW.sys_created := clock_timestamp();
|
|
818
|
+
NEW.sys_modified := clock_timestamp();
|
|
819
|
+
NEW.sys_modified_count := 0;
|
|
820
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
821
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
822
|
+
NEW.sys_created := OLD.sys_created;
|
|
823
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0);
|
|
824
|
+
IF ROW(NEW.*) IS DISTINCT FROM ROW(OLD.*) THEN
|
|
825
|
+
IF OLD.sys_dirty IS TRUE AND NEW.sys_dirty IS FALSE THEN
|
|
826
|
+
NEW.sys_dirty := FALSE;
|
|
827
|
+
ELSE
|
|
828
|
+
NEW.sys_dirty := TRUE;
|
|
829
|
+
END IF;
|
|
830
|
+
NEW.sys_modified := clock_timestamp();
|
|
831
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
|
|
832
|
+
END IF;
|
|
833
|
+
END IF;
|
|
834
|
+
RETURN NEW;
|
|
835
|
+
END;
|
|
836
|
+
$BODY$
|
|
837
|
+
LANGUAGE plpgsql VOLATILE
|
|
838
|
+
COST 100;
|
|
839
|
+
""",
|
|
840
|
+
f"DROP TRIGGER IF EXISTS {trigger_name} ON {fqtn};",
|
|
841
|
+
f"""
|
|
842
|
+
CREATE TRIGGER {trigger_name}
|
|
843
|
+
BEFORE INSERT OR UPDATE ON {fqtn}
|
|
844
|
+
FOR EACH ROW EXECUTE PROCEDURE {schema}.on_sys_modified();
|
|
845
|
+
""",
|
|
846
|
+
]
|
|
847
|
+
|
|
848
|
+
sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
849
|
+
return sql, tuple()
|
|
850
|
+
|
|
785
851
|
@classmethod
|
|
786
852
|
def drop_table(cls, name):
|
|
787
853
|
return f"drop table if exists {TableHelper.quote(name)} cascade;", tuple()
|
|
@@ -18,6 +18,18 @@ TableHelper.reserved = reserved_words
|
|
|
18
18
|
TableHelper.operators = OPERATORS
|
|
19
19
|
|
|
20
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
|
+
|
|
21
33
|
def quote(data):
|
|
22
34
|
"""Quote SQLite identifiers."""
|
|
23
35
|
if isinstance(data, list):
|
|
@@ -344,11 +356,122 @@ class SQL(BaseSQLDialect):
|
|
|
344
356
|
|
|
345
357
|
@classmethod
|
|
346
358
|
def create_table(cls, name, columns=None, drop=False):
|
|
359
|
+
if not name or not isinstance(name, str):
|
|
360
|
+
raise ValueError("Table name must be a non-empty string")
|
|
361
|
+
|
|
362
|
+
columns = columns or {}
|
|
363
|
+
table_identifier = quote(name)
|
|
364
|
+
base_name = name.split(".")[-1].replace('"', "")
|
|
365
|
+
base_name_sql = base_name.replace("'", "''")
|
|
366
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
|
|
367
|
+
|
|
368
|
+
statements = []
|
|
347
369
|
if drop:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
370
|
+
statements.append(f"DROP TABLE IF EXISTS {table_identifier};")
|
|
371
|
+
|
|
372
|
+
statements.append(
|
|
373
|
+
f"""
|
|
374
|
+
CREATE TABLE {table_identifier} (
|
|
375
|
+
"sys_id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
376
|
+
"sys_table" TEXT,
|
|
377
|
+
"sys_created" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
378
|
+
"sys_modified" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
379
|
+
"sys_modified_by" TEXT,
|
|
380
|
+
"sys_modified_count" INTEGER NOT NULL DEFAULT 0,
|
|
381
|
+
"sys_dirty" INTEGER NOT NULL DEFAULT 0,
|
|
382
|
+
"description" TEXT
|
|
383
|
+
);
|
|
384
|
+
""".strip()
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for key, val in columns.items():
|
|
388
|
+
clean_key = re.sub("<>!=%", "", key)
|
|
389
|
+
if clean_key in system_fields:
|
|
390
|
+
continue
|
|
391
|
+
col_type = TYPES.get_type(val)
|
|
392
|
+
statements.append(
|
|
393
|
+
f"ALTER TABLE {table_identifier} ADD COLUMN {quote(clean_key)} {col_type};"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
statements.extend(
|
|
397
|
+
[
|
|
398
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_ai;",
|
|
399
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_au;",
|
|
400
|
+
f"""
|
|
401
|
+
CREATE TRIGGER {trigger_prefix}_ai
|
|
402
|
+
AFTER INSERT ON {table_identifier}
|
|
403
|
+
FOR EACH ROW
|
|
404
|
+
BEGIN
|
|
405
|
+
UPDATE {table_identifier}
|
|
406
|
+
SET sys_created = COALESCE(NEW.sys_created, CURRENT_TIMESTAMP),
|
|
407
|
+
sys_modified = CURRENT_TIMESTAMP,
|
|
408
|
+
sys_modified_count = 0,
|
|
409
|
+
sys_dirty = COALESCE(NEW.sys_dirty, 0),
|
|
410
|
+
sys_table = '{base_name_sql}'
|
|
411
|
+
WHERE rowid = NEW.rowid;
|
|
412
|
+
END;
|
|
413
|
+
""".strip(),
|
|
414
|
+
f"""
|
|
415
|
+
CREATE TRIGGER {trigger_prefix}_au
|
|
416
|
+
AFTER UPDATE ON {table_identifier}
|
|
417
|
+
FOR EACH ROW
|
|
418
|
+
BEGIN
|
|
419
|
+
UPDATE {table_identifier}
|
|
420
|
+
SET sys_created = OLD.sys_created,
|
|
421
|
+
sys_modified = CURRENT_TIMESTAMP,
|
|
422
|
+
sys_table = '{base_name_sql}',
|
|
423
|
+
sys_dirty = CASE WHEN OLD.sys_dirty = 1 AND NEW.sys_dirty = 0 THEN 0 ELSE 1 END,
|
|
424
|
+
sys_modified_count = CASE WHEN OLD.sys_dirty = 1 AND NEW.sys_dirty = 0 THEN COALESCE(OLD.sys_modified_count, 0) ELSE COALESCE(OLD.sys_modified_count, 0) + 1 END
|
|
425
|
+
WHERE rowid = NEW.rowid;
|
|
426
|
+
END;
|
|
427
|
+
""".strip(),
|
|
428
|
+
]
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return "\n".join(statements), tuple()
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def ensure_sys_modified_count(cls, name):
|
|
435
|
+
"""Ensure sys_modified_count exists for SQLite tables."""
|
|
436
|
+
table_identifier = quote(name)
|
|
437
|
+
base_name = name.split(".")[-1].replace('"', "")
|
|
438
|
+
base_name_sql = base_name.replace("'", "''")
|
|
439
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
|
|
440
|
+
statements = [
|
|
441
|
+
f"ALTER TABLE {table_identifier} ADD COLUMN sys_modified_count INTEGER NOT NULL DEFAULT 0;",
|
|
442
|
+
f"UPDATE {table_identifier} SET sys_modified_count = 0 WHERE sys_modified_count IS NULL;",
|
|
443
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_ai;",
|
|
444
|
+
f"DROP TRIGGER IF EXISTS {trigger_prefix}_au;",
|
|
445
|
+
f"""
|
|
446
|
+
CREATE TRIGGER {trigger_prefix}_ai
|
|
447
|
+
AFTER INSERT ON {table_identifier}
|
|
448
|
+
FOR EACH ROW
|
|
449
|
+
BEGIN
|
|
450
|
+
UPDATE {table_identifier}
|
|
451
|
+
SET sys_created = COALESCE(NEW.sys_created, CURRENT_TIMESTAMP),
|
|
452
|
+
sys_modified = CURRENT_TIMESTAMP,
|
|
453
|
+
sys_modified_count = 0,
|
|
454
|
+
sys_dirty = COALESCE(NEW.sys_dirty, 0),
|
|
455
|
+
sys_table = '{base_name_sql}'
|
|
456
|
+
WHERE rowid = NEW.rowid;
|
|
457
|
+
END;
|
|
458
|
+
""".strip(),
|
|
459
|
+
f"""
|
|
460
|
+
CREATE TRIGGER {trigger_prefix}_au
|
|
461
|
+
AFTER UPDATE ON {table_identifier}
|
|
462
|
+
FOR EACH ROW
|
|
463
|
+
BEGIN
|
|
464
|
+
UPDATE {table_identifier}
|
|
465
|
+
SET sys_created = OLD.sys_created,
|
|
466
|
+
sys_modified = CURRENT_TIMESTAMP,
|
|
467
|
+
sys_table = '{base_name_sql}',
|
|
468
|
+
sys_dirty = CASE WHEN OLD.sys_dirty = 1 AND NEW.sys_dirty = 0 THEN 0 ELSE 1 END,
|
|
469
|
+
sys_modified_count = CASE WHEN OLD.sys_dirty = 1 AND NEW.sys_dirty = 0 THEN COALESCE(OLD.sys_modified_count, 0) ELSE COALESCE(OLD.sys_modified_count, 0) + 1 END
|
|
470
|
+
WHERE rowid = NEW.rowid;
|
|
471
|
+
END;
|
|
472
|
+
""".strip(),
|
|
473
|
+
]
|
|
474
|
+
return "\n".join(statements), tuple()
|
|
352
475
|
|
|
353
476
|
@classmethod
|
|
354
477
|
def drop_table(cls, name):
|