velocity-python 0.0.154__tar.gz → 0.0.156__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.
Files changed (136) hide show
  1. {velocity_python-0.0.154 → velocity_python-0.0.156}/PKG-INFO +1 -1
  2. {velocity_python-0.0.154 → velocity_python-0.0.156}/pyproject.toml +1 -1
  3. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/engine.py +3 -2
  5. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/table.py +39 -14
  6. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/transaction.py +9 -4
  7. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/mysql/sql.py +32 -10
  8. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/postgres/sql.py +165 -91
  9. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlite/sql.py +34 -10
  10. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlserver/sql.py +31 -12
  11. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_table_comprehensive.py +3 -1
  12. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_db_utils.py +49 -0
  13. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/utils.py +67 -4
  14. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity_python.egg-info/PKG-INFO +1 -1
  15. {velocity_python-0.0.154 → velocity_python-0.0.156}/tests/test_sys_modified_count_postgres_demo.py +2 -2
  16. {velocity_python-0.0.154 → velocity_python-0.0.156}/LICENSE +0 -0
  17. {velocity_python-0.0.154 → velocity_python-0.0.156}/README.md +0 -0
  18. {velocity_python-0.0.154 → velocity_python-0.0.156}/setup.cfg +0 -0
  19. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/__init__.py +0 -0
  20. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/invoices.py +0 -0
  21. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/orders.py +0 -0
  22. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/payments.py +0 -0
  23. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/purchase_orders.py +0 -0
  24. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/tests/__init__.py +0 -0
  25. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/tests/test_email_processing.py +0 -0
  26. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  27. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  28. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/__init__.py +0 -0
  29. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/amplify.py +0 -0
  30. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/__init__.py +0 -0
  31. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/base_handler.py +0 -0
  32. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/context.py +0 -0
  33. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/exceptions.py +0 -0
  34. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  35. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  36. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/mixins/activity_tracker.py +0 -0
  37. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/mixins/aws_session_mixin.py +0 -0
  38. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
  39. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
  40. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
  41. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/response.py +0 -0
  42. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  43. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/tests/__init__.py +0 -0
  44. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  45. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/aws/tests/test_response.py +0 -0
  46. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/__init__.py +0 -0
  47. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/__init__.py +0 -0
  48. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/column.py +0 -0
  49. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/database.py +0 -0
  50. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/decorators.py +0 -0
  51. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/result.py +0 -0
  52. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/row.py +0 -0
  53. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/core/sequence.py +0 -0
  54. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/exceptions.py +0 -0
  55. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/__init__.py +0 -0
  56. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/base/__init__.py +0 -0
  57. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/base/initializer.py +0 -0
  58. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/base/operators.py +0 -0
  59. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/base/sql.py +0 -0
  60. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/base/types.py +0 -0
  61. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/mysql/__init__.py +0 -0
  62. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/mysql/operators.py +0 -0
  63. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/mysql/reserved.py +0 -0
  64. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/mysql/types.py +0 -0
  65. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/postgres/__init__.py +0 -0
  66. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/postgres/operators.py +0 -0
  67. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/postgres/reserved.py +0 -0
  68. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/postgres/types.py +0 -0
  69. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  70. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlite/operators.py +0 -0
  71. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  72. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlite/types.py +0 -0
  73. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  74. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  75. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  76. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/sqlserver/types.py +0 -0
  77. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/servers/tablehelper.py +0 -0
  78. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/__init__.py +0 -0
  79. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/common_db_test.py +0 -0
  80. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/__init__.py +0 -0
  81. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/common.py +0 -0
  82. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_column.py +0 -0
  83. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  84. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_database.py +0 -0
  85. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  86. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  87. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  88. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_result.py +0 -0
  89. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_row.py +0 -0
  90. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  91. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  92. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  93. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  94. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  95. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_table.py +0 -0
  96. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  97. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/sql/__init__.py +0 -0
  98. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/sql/common.py +0 -0
  99. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  100. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  101. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  102. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_postgres.py +0 -0
  103. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  104. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  105. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_result_caching.py +0 -0
  106. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  107. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  108. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  109. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  110. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_sql_builder.py +0 -0
  111. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/db/tests/test_tablehelper.py +0 -0
  112. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/__init__.py +0 -0
  113. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/conv/__init__.py +0 -0
  114. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/conv/iconv.py +0 -0
  115. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/conv/oconv.py +0 -0
  116. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/db.py +0 -0
  117. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/export.py +0 -0
  118. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/format.py +0 -0
  119. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/mail.py +0 -0
  120. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/merge.py +0 -0
  121. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/__init__.py +0 -0
  122. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_db.py +0 -0
  123. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_fix.py +0 -0
  124. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_format.py +0 -0
  125. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_iconv.py +0 -0
  126. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_merge.py +0 -0
  127. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_oconv.py +0 -0
  128. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_original_error.py +0 -0
  129. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tests/test_timer.py +0 -0
  130. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/timer.py +0 -0
  131. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity/misc/tools.py +0 -0
  132. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  133. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  134. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity_python.egg-info/requires.txt +0 -0
  135. {velocity_python-0.0.154 → velocity_python-0.0.156}/src/velocity_python.egg-info/top_level.txt +0 -0
  136. {velocity_python-0.0.154 → velocity_python-0.0.156}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.154
3
+ Version: 0.0.156
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.154"
7
+ version = "0.0.156"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.154"
1
+ __version__ = version = "0.0.156"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -1,10 +1,10 @@
1
1
  import inspect
2
2
  import re
3
- import os
4
3
  from contextlib import contextmanager
5
4
  from functools import wraps
6
5
  from velocity.db import exceptions
7
6
  from velocity.db.core.transaction import Transaction
7
+ from velocity.db.utils import mask_config_for_display
8
8
 
9
9
  import logging
10
10
 
@@ -27,7 +27,8 @@ class Engine:
27
27
  self.__schema_locked = schema_locked
28
28
 
29
29
  def __str__(self):
30
- return f"[{self.sql.server}] engine({self.config})"
30
+ safe_config = mask_config_for_display(self.config)
31
+ return f"[{self.sql.server}] engine({safe_config})"
31
32
 
32
33
  def connect(self):
33
34
  """
@@ -24,7 +24,24 @@ class Query:
24
24
  return self.sql
25
25
 
26
26
 
27
+ SYSTEM_COLUMN_NAMES = (
28
+ "sys_id",
29
+ "sys_created",
30
+ "sys_modified",
31
+ "sys_modified_by",
32
+ "sys_modified_row",
33
+ "sys_modified_count",
34
+ "sys_dirty",
35
+ "sys_table",
36
+ "description",
37
+ )
38
+
39
+ _SYSTEM_COLUMN_SET = {name.lower() for name in SYSTEM_COLUMN_NAMES}
40
+
41
+
27
42
  class Table:
43
+ SYSTEM_COLUMNS = SYSTEM_COLUMN_NAMES
44
+
28
45
  """
29
46
  Provides an interface for performing CRUD and metadata operations on a DB table.
30
47
  """
@@ -94,9 +111,15 @@ class Table:
94
111
 
95
112
  def columns(self):
96
113
  """
97
- Returns column names, excluding columns that start with 'sys_'.
114
+ Returns non-system column names.
98
115
  """
99
- return [col for col in self.sys_columns() if not col.startswith("sys_")]
116
+ return [col for col in self.sys_columns() if not self.is_system_column(col)]
117
+
118
+ @staticmethod
119
+ def is_system_column(column_name):
120
+ if not column_name:
121
+ return False
122
+ return column_name.lower() in _SYSTEM_COLUMN_SET or column_name.lower().startswith("sys_")
100
123
 
101
124
  @return_default(None, (exceptions.DbObjectExistsError,))
102
125
  def create_index(
@@ -217,12 +240,8 @@ class Table:
217
240
  return self.name in [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
218
241
  return self.name in [x[1] for x in result.as_tuple()]
219
242
 
220
- def ensure_sys_modified_count(self, **kwds):
221
- """
222
- Ensure the sys_modified_count column and trigger exist for this table.
223
-
224
- Returns early when the column is already present unless `force=True` is provided.
225
- """
243
+ def ensure_system_columns(self, **kwds):
244
+ """Ensure Velocity system columns and triggers exist for this table."""
226
245
  force = kwds.get("force", False)
227
246
 
228
247
  try:
@@ -230,15 +249,21 @@ class Table:
230
249
  except Exception:
231
250
  columns = []
232
251
 
233
- has_column = "sys_modified_count" in columns
234
- has_row_column = "sys_modified_row" in columns
252
+ sql_method = getattr(self.sql, "ensure_system_columns", None)
235
253
 
236
- if has_column and has_row_column and not force:
237
- return
254
+ if sql_method is None:
255
+ raise AttributeError(
256
+ f"{self.sql.__class__.__name__} does not implement ensure_system_columns"
257
+ )
238
258
 
239
- sql, vals = self.sql.ensure_sys_modified_count(
240
- self.name, has_column=has_column, has_row_column=has_row_column
259
+ result = sql_method(
260
+ self.name, existing_columns=columns, force=force
241
261
  )
262
+
263
+ if not result:
264
+ return
265
+
266
+ sql, vals = result
242
267
  if kwds.get("sql_only", False):
243
268
  return sql, vals
244
269
  self.tx.execute(sql, vals, cursor=self.cursor())
@@ -6,6 +6,7 @@ from velocity.db.core.result import Result
6
6
  from velocity.db.core.column import Column
7
7
  from velocity.db.core.database import Database
8
8
  from velocity.db.core.sequence import Sequence
9
+ from velocity.db.utils import mask_config_for_display
9
10
  from velocity.misc.db import randomword
10
11
 
11
12
  debug = False
@@ -22,10 +23,14 @@ class Transaction:
22
23
  self.__pg_types = {}
23
24
 
24
25
  def __str__(self):
25
- c = self.engine.config
26
- server = c.get("host", c.get("server"))
27
- database = c.get("database")
28
- return f"{self.engine.sql.server}.transaction({server}:{database})"
26
+ config = mask_config_for_display(self.engine.config)
27
+
28
+ if isinstance(config, dict):
29
+ server = config.get("host", config.get("server"))
30
+ database = config.get("database", config.get("dbname"))
31
+ return f"{self.engine.sql.server}.transaction({server}:{database})"
32
+
33
+ return f"{self.engine.sql.server}.transaction({config})"
29
34
 
30
35
  def __enter__(self):
31
36
  return self
@@ -450,19 +450,40 @@ END;
450
450
  return "\n".join(statements), tuple()
451
451
 
452
452
  @classmethod
453
- def ensure_sys_modified_count(cls, name, has_column=False, has_row_column=False):
454
- """Ensure sys_modified_count column and associated triggers exist for the table."""
453
+ def ensure_system_columns(cls, name, existing_columns=None, force=False):
454
+ """Ensure MySQL tables maintain the Velocity system metadata."""
455
+ existing_columns = {col.lower() for col in existing_columns or []}
456
+
455
457
  table_identifier = quote(name)
456
458
  base_name = name.split(".")[-1].replace("`", "")
457
459
  base_name_sql = base_name.replace("'", "''")
458
460
  trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
459
461
 
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"""
462
+ has_count = "sys_modified_count" in existing_columns
463
+
464
+ add_column = not has_count
465
+ recreate_triggers = force or add_column
466
+
467
+ if not recreate_triggers and not force:
468
+ return None
469
+
470
+ statements = []
471
+
472
+ if add_column:
473
+ statements.append(
474
+ f"ALTER TABLE {table_identifier} ADD COLUMN IF NOT EXISTS `sys_modified_count` INT NOT NULL DEFAULT 0;"
475
+ )
476
+
477
+ statements.append(
478
+ f"UPDATE {table_identifier} SET `sys_modified_count` = 0 WHERE `sys_modified_count` IS NULL;"
479
+ )
480
+
481
+ statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_bi;")
482
+ statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_bu;")
483
+
484
+ statements.extend(
485
+ [
486
+ f"""
466
487
  CREATE TRIGGER {trigger_prefix}_bi
467
488
  BEFORE INSERT ON {table_identifier}
468
489
  FOR EACH ROW
@@ -474,7 +495,7 @@ BEGIN
474
495
  SET NEW.sys_table = '{base_name_sql}';
475
496
  END;
476
497
  """.strip(),
477
- f"""
498
+ f"""
478
499
  CREATE TRIGGER {trigger_prefix}_bu
479
500
  BEFORE UPDATE ON {table_identifier}
480
501
  FOR EACH ROW
@@ -491,7 +512,8 @@ BEGIN
491
512
  SET NEW.sys_table = '{base_name_sql}';
492
513
  END;
493
514
  """.strip(),
494
- ]
515
+ ]
516
+ )
495
517
 
496
518
  return "\n".join(statements), tuple()
497
519
 
@@ -23,6 +23,8 @@ system_fields = [
23
23
  "sys_created",
24
24
  "sys_modified",
25
25
  "sys_modified_by",
26
+ "sys_modified_row",
27
+ "sys_modified_count",
26
28
  "sys_dirty",
27
29
  "sys_table",
28
30
  "description",
@@ -799,37 +801,13 @@ class SQL(BaseSQLDialect):
799
801
  def drop_database(cls, name):
800
802
  return f"drop database if exists {name}", tuple()
801
803
 
802
- @classmethod
803
- def create_table(cls, name, columns={}, drop=False):
804
- if "." in name:
805
- fqtn = TableHelper.quote(name)
806
- else:
807
- fqtn = f"public.{TableHelper.quote(name)}"
808
- schema, table = fqtn.split(".")
809
- name = fqtn.replace(".", "_")
810
- sql = []
811
- if drop:
812
- sql.append(cls.drop_table(fqtn)[0])
813
- sql.append(
814
- f"""
815
- CREATE TABLE {fqtn} (
816
- sys_id BIGSERIAL PRIMARY KEY,
817
- sys_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
818
- sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
819
- sys_modified_by TEXT NOT NULL DEFAULT 'SYSTEM',
820
- sys_modified_row TIMESTAMP NOT NULL DEFAULT CLOCK_TIMESTAMP(),
821
- sys_modified_count INTEGER NOT NULL DEFAULT 0,
822
- sys_dirty BOOLEAN NOT NULL DEFAULT FALSE,
823
- sys_table TEXT NOT NULL,
824
- description TEXT
825
- );
826
-
827
- SELECT SETVAL(PG_GET_SERIAL_SEQUENCE('{fqtn}', 'sys_id'),1000,TRUE);
828
-
829
- CREATE OR REPLACE FUNCTION {schema}.on_sys_modified()
804
+ @staticmethod
805
+ def _sys_modified_function_sql(schema_identifier):
806
+ return f"""
807
+ CREATE OR REPLACE FUNCTION {schema_identifier}.on_sys_modified()
830
808
  RETURNS TRIGGER AS
831
809
  $BODY$
832
- BEGIN
810
+ BEGIN
833
811
  IF (TG_OP = 'INSERT') THEN
834
812
  NEW.sys_table := TG_TABLE_NAME;
835
813
  NEW.sys_created := transaction_timestamp();
@@ -847,19 +825,58 @@ class SQL(BaseSQLDialect):
847
825
  NEW.sys_dirty := TRUE;
848
826
  END IF;
849
827
  NEW.sys_modified := transaction_timestamp();
828
+ NEW.sys_modified_row := clock_timestamp();
850
829
  NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
851
830
  END IF;
852
831
  END IF;
853
-
854
832
  RETURN NEW;
855
- END;
833
+ END;
856
834
  $BODY$
857
835
  LANGUAGE plpgsql VOLATILE
858
836
  COST 100;
837
+ """
838
+
839
+ @classmethod
840
+ def create_table(cls, name, columns={}, drop=False):
841
+ if "." in name:
842
+ fqtn = TableHelper.quote(name)
843
+ else:
844
+ fqtn = f"public.{TableHelper.quote(name)}"
859
845
 
860
- CREATE TRIGGER on_update_row_{fqtn.replace('.', '_')}
846
+ schema, table = fqtn.split(".")
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)
854
+ sql = []
855
+ if drop:
856
+ sql.append(cls.drop_table(fqtn)[0])
857
+ sql.append(
858
+ f"""
859
+ CREATE TABLE {fqtn} (
860
+ sys_id BIGSERIAL PRIMARY KEY,
861
+ sys_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
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,
866
+ sys_dirty BOOLEAN NOT NULL DEFAULT FALSE,
867
+ sys_table TEXT NOT NULL,
868
+ description TEXT
869
+ );
870
+
871
+ SELECT SETVAL(PG_GET_SERIAL_SEQUENCE('{fqtn}', 'sys_id'),1000,TRUE);
872
+
873
+ {cls._sys_modified_function_sql(schema_identifier)}
874
+
875
+ DROP TRIGGER IF EXISTS {trigger_identifier} ON {fqtn};
876
+
877
+ CREATE TRIGGER {trigger_identifier}
861
878
  BEFORE INSERT OR UPDATE ON {fqtn}
862
- FOR EACH ROW EXECUTE PROCEDURE {schema}.on_sys_modified();
879
+ FOR EACH ROW EXECUTE PROCEDURE {schema_identifier}.on_sys_modified();
863
880
 
864
881
  """
865
882
  )
@@ -876,81 +893,138 @@ class SQL(BaseSQLDialect):
876
893
  return sql, tuple()
877
894
 
878
895
  @classmethod
879
- def ensure_sys_modified_count(
880
- cls, name, has_column=False, has_row_column=False
881
- ):
882
- """Return SQL to backfill sys_modified_count/sys_modified_row and refresh the on_sys_modified trigger."""
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
+
883
921
  if "." in name:
884
922
  schema_name, table_name = name.split(".", 1)
885
923
  else:
886
924
  schema_name = cls.default_schema
887
925
  table_name = name
888
926
 
889
- schema_identifier = TableHelper.quote(schema_name)
890
- table_identifier = TableHelper.quote(table_name)
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)
891
932
  fqtn = f"{schema_identifier}.{table_identifier}"
892
933
 
893
934
  trigger_name = (
894
- f"on_update_row_{schema_name}_{table_name}"
895
- .replace(".", "_")
896
- .replace('"', "")
935
+ f"on_update_row_{schema_unquoted}_{table_unquoted}".replace(".", "_")
897
936
  )
898
937
  trigger_identifier = TableHelper.quote(trigger_name)
899
- column_name = TableHelper.quote("sys_modified_count")
900
- row_column_name = TableHelper.quote("sys_modified_row")
901
-
938
+
902
939
  statements = []
903
- if not has_column:
940
+ added_columns = set()
941
+ columns_after = set(existing_columns)
942
+
943
+ if "sys_id" in missing_columns:
904
944
  statements.append(
905
- f"ALTER TABLE {fqtn} ADD COLUMN {column_name} INTEGER NOT NULL DEFAULT 0;"
945
+ f"ALTER TABLE {fqtn} ADD COLUMN {TableHelper.quote('sys_id')} BIGSERIAL PRIMARY KEY;"
906
946
  )
907
- if not has_row_column:
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("'", "''")
908
991
  statements.append(
909
- f"ALTER TABLE {fqtn} ADD COLUMN {row_column_name} TIMESTAMPTZ;"
992
+ f"UPDATE {fqtn} SET {quoted_column} = COALESCE({quoted_column}, '{table_literal}') WHERE {quoted_column} IS NULL;"
910
993
  )
911
994
 
912
- statements.extend([
913
- f"UPDATE {fqtn} SET {column_name} = 0 WHERE {column_name} IS NULL;",
914
- f"UPDATE {fqtn} SET {row_column_name} = COALESCE({row_column_name}, clock_timestamp());",
915
- f"""
916
- CREATE OR REPLACE FUNCTION {schema_identifier}.on_sys_modified()
917
- RETURNS TRIGGER AS
918
- $BODY$
919
- BEGIN
920
- IF (TG_OP = 'INSERT') THEN
921
- NEW.sys_table := TG_TABLE_NAME;
922
- NEW.sys_created := transaction_timestamp();
923
- NEW.sys_modified := transaction_timestamp();
924
- NEW.sys_modified_row := clock_timestamp();
925
- NEW.sys_modified_count := 0;
926
- ELSIF (TG_OP = 'UPDATE') THEN
927
- NEW.sys_table := TG_TABLE_NAME;
928
- NEW.sys_created := OLD.sys_created;
929
- NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0);
930
- IF ROW(NEW.*) IS DISTINCT FROM ROW(OLD.*) THEN
931
- IF OLD.sys_dirty IS TRUE AND NEW.sys_dirty IS FALSE THEN
932
- NEW.sys_dirty := FALSE;
933
- ELSE
934
- NEW.sys_dirty := TRUE;
935
- END IF;
936
- NEW.sys_modified := transaction_timestamp();
937
- NEW.sys_modified_row := clock_timestamp();
938
- NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
939
- END IF;
940
- END IF;
941
- RETURN NEW;
942
- END;
943
- $BODY$
944
- LANGUAGE plpgsql VOLATILE
945
- COST 100;
946
- """,
947
- f"DROP TRIGGER IF EXISTS {trigger_identifier} ON {fqtn};",
948
- f"""
949
- CREATE TRIGGER {trigger_identifier}
950
- BEFORE INSERT OR UPDATE ON {fqtn}
951
- FOR EACH ROW EXECUTE PROCEDURE {schema_identifier}.on_sys_modified();
952
- """,
953
- ])
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
954
1028
 
955
1029
  sql = sqlparse.format(
956
1030
  " ".join(statements), reindent=True, keyword_case="upper"
@@ -431,18 +431,40 @@ END;
431
431
  return "\n".join(statements), tuple()
432
432
 
433
433
  @classmethod
434
- def ensure_sys_modified_count(cls, name, has_column=False, has_row_column=False):
435
- """Ensure sys_modified_count exists for SQLite tables."""
434
+ def ensure_system_columns(cls, name, existing_columns=None, force=False):
435
+ """Ensure SQLite tables maintain the Velocity system triggers/columns."""
436
+ existing_columns = {col.lower() for col in existing_columns or []}
437
+
436
438
  table_identifier = quote(name)
437
439
  base_name = name.split(".")[-1].replace('"', "")
438
440
  base_name_sql = base_name.replace("'", "''")
439
441
  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"""
442
+
443
+ has_count = "sys_modified_count" in existing_columns
444
+
445
+ add_column = not has_count
446
+ recreate_triggers = force or add_column
447
+
448
+ if not recreate_triggers and not force:
449
+ return None
450
+
451
+ statements = []
452
+
453
+ if add_column:
454
+ statements.append(
455
+ f"ALTER TABLE {table_identifier} ADD COLUMN sys_modified_count INTEGER NOT NULL DEFAULT 0;"
456
+ )
457
+
458
+ statements.append(
459
+ f"UPDATE {table_identifier} SET sys_modified_count = 0 WHERE sys_modified_count IS NULL;"
460
+ )
461
+
462
+ statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_ai;")
463
+ statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_au;")
464
+
465
+ statements.extend(
466
+ [
467
+ f"""
446
468
  CREATE TRIGGER {trigger_prefix}_ai
447
469
  AFTER INSERT ON {table_identifier}
448
470
  FOR EACH ROW
@@ -456,7 +478,7 @@ BEGIN
456
478
  WHERE rowid = NEW.rowid;
457
479
  END;
458
480
  """.strip(),
459
- f"""
481
+ f"""
460
482
  CREATE TRIGGER {trigger_prefix}_au
461
483
  AFTER UPDATE ON {table_identifier}
462
484
  FOR EACH ROW
@@ -470,7 +492,9 @@ BEGIN
470
492
  WHERE rowid = NEW.rowid;
471
493
  END;
472
494
  """.strip(),
473
- ]
495
+ ]
496
+ )
497
+
474
498
  return "\n".join(statements), tuple()
475
499
 
476
500
  @classmethod