velocity-python 0.0.140__py3-none-any.whl → 0.0.142__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

velocity/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.140"
1
+ __version__ = version = "0.0.142"
2
2
 
3
3
  from . import aws
4
4
  from . import db
velocity/db/core/table.py CHANGED
@@ -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
- drop_sql = f"DROP TABLE IF EXISTS {quote(name)}"
366
- return drop_sql
367
-
368
- # Basic CREATE TABLE - would need more implementation
369
- return f"CREATE TABLE {quote(name)} (id INT PRIMARY KEY AUTO_INCREMENT)"
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
- sys_modified_by TEXT,
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
- BEGIN
751
- -- update sys_modified on each insert/update.
752
- NEW.sys_modified := now();
753
- if (TG_OP = 'INSERT') THEN
754
- NEW.sys_created :=now();
755
- ELSEIF (TG_OP = 'UDPATE') THEN
756
- -- Do not allow sys_created to be modified.
757
- NEW.sys_created := OLD.sys_created;
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
- -- Insert table name to row
760
- NEW.sys_table := TG_TABLE_NAME;
761
- RETURN NEW;
762
- END;
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
- return f"DROP TABLE IF EXISTS {quote(name)}"
349
-
350
- # Basic CREATE TABLE
351
- return f"CREATE TABLE {quote(name)} (id INTEGER PRIMARY KEY AUTOINCREMENT)"
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):
@@ -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 SQL Server identifiers."""
23
35
  if isinstance(data, list):
@@ -383,11 +395,153 @@ class SQL(BaseSQLDialect):
383
395
 
384
396
  @classmethod
385
397
  def create_table(cls, name, columns=None, drop=False):
398
+ if not name or not isinstance(name, str):
399
+ raise ValueError("Table name must be a non-empty string")
400
+
401
+ columns = columns or {}
402
+
403
+ if "." in name:
404
+ schema_part, table_part = name.split(".", 1)
405
+ else:
406
+ schema_part = cls.default_schema or "dbo"
407
+ table_part = name
408
+
409
+ schema_identifier = quote(schema_part)
410
+ table_identifier = quote(name if "." in name else f"{schema_part}.{table_part}")
411
+ base_name = table_part.replace("[", "").replace("]", "")
412
+ base_name_sql = base_name.replace("'", "''")
413
+ trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"CC_SYS_MOD_{base_name}")
414
+
415
+ statements = []
386
416
  if drop:
387
- return f"DROP TABLE IF EXISTS {quote(name)}"
388
-
389
- # Basic CREATE TABLE
390
- return f"CREATE TABLE {quote(name)} (id INT IDENTITY(1,1) PRIMARY KEY)"
417
+ statements.append(f"IF OBJECT_ID(N'{table_identifier}', N'U') IS NOT NULL DROP TABLE {table_identifier};")
418
+
419
+ statements.append(
420
+ f"""
421
+ CREATE TABLE {table_identifier} (
422
+ [sys_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
423
+ [sys_table] NVARCHAR(255),
424
+ [sys_created] DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
425
+ [sys_modified] DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
426
+ [sys_modified_by] NVARCHAR(255),
427
+ [sys_modified_count] INT NOT NULL DEFAULT 0,
428
+ [sys_dirty] BIT NOT NULL DEFAULT 0,
429
+ [description] NVARCHAR(MAX)
430
+ );
431
+ """.strip()
432
+ )
433
+
434
+ for key, val in columns.items():
435
+ clean_key = re.sub("<>!=%", "", key)
436
+ if clean_key in system_fields:
437
+ continue
438
+ col_type = TYPES.get_type(val)
439
+ statements.append(
440
+ f"ALTER TABLE {table_identifier} ADD {quote(clean_key)} {col_type};"
441
+ )
442
+
443
+ statements.extend(
444
+ [
445
+ f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_insert', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_insert;",
446
+ f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_update', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_update;",
447
+ f"""
448
+ CREATE TRIGGER {schema_identifier}.{trigger_prefix}_insert
449
+ ON {table_identifier}
450
+ AFTER INSERT
451
+ AS
452
+ BEGIN
453
+ SET NOCOUNT ON;
454
+ UPDATE t
455
+ SET sys_created = ISNULL(i.sys_created, SYSDATETIME()),
456
+ sys_modified = SYSDATETIME(),
457
+ sys_modified_count = 0,
458
+ sys_dirty = ISNULL(i.sys_dirty, 0),
459
+ sys_table = '{base_name_sql}'
460
+ FROM {table_identifier} AS t
461
+ INNER JOIN inserted AS i ON t.sys_id = i.sys_id;
462
+ END;
463
+ """.strip(),
464
+ f"""
465
+ CREATE TRIGGER {schema_identifier}.{trigger_prefix}_update
466
+ ON {table_identifier}
467
+ AFTER UPDATE
468
+ AS
469
+ BEGIN
470
+ SET NOCOUNT ON;
471
+ UPDATE t
472
+ SET sys_created = d.sys_created,
473
+ sys_modified = SYSDATETIME(),
474
+ sys_table = '{base_name_sql}',
475
+ sys_dirty = CASE WHEN d.sys_dirty = 1 AND i.sys_dirty = 0 THEN 0 ELSE 1 END,
476
+ sys_modified_count = CASE WHEN d.sys_dirty = 1 AND i.sys_dirty = 0 THEN ISNULL(d.sys_modified_count, 0) ELSE ISNULL(d.sys_modified_count, 0) + 1 END
477
+ FROM {table_identifier} AS t
478
+ INNER JOIN inserted AS i ON t.sys_id = i.sys_id
479
+ INNER JOIN deleted AS d ON d.sys_id = i.sys_id;
480
+ END;
481
+ """.strip(),
482
+ ]
483
+ )
484
+
485
+ return "\n".join(statements), tuple()
486
+
487
+ @classmethod
488
+ def ensure_sys_modified_count(cls, name):
489
+ """Ensure sys_modified_count exists for SQL Server tables along with maintenance triggers."""
490
+ if "." in name:
491
+ schema, table_name = name.split(".", 1)
492
+ else:
493
+ schema = cls.default_schema or "dbo"
494
+ table_name = name
495
+
496
+ schema_identifier = quote(schema)
497
+ table_identifier = quote(name if "." in name else f"{schema}.{table_name}")
498
+ object_name = f"[{schema}].[{table_name}]"
499
+ table_name_sql = table_name.replace("'", "''")
500
+ trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"CC_SYS_MOD_{table_name}")
501
+
502
+ statements = [
503
+ f"IF COL_LENGTH(N'{object_name}', 'sys_modified_count') IS NULL BEGIN ALTER TABLE {table_identifier} ADD sys_modified_count INT NOT NULL CONSTRAINT DF_{trigger_prefix}_COUNT DEFAULT (0); END;",
504
+ f"UPDATE {table_identifier} SET sys_modified_count = 0 WHERE sys_modified_count IS NULL;",
505
+ f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_insert', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_insert;",
506
+ f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_update', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_update;",
507
+ f"""
508
+ CREATE TRIGGER {schema_identifier}.{trigger_prefix}_insert
509
+ ON {table_identifier}
510
+ AFTER INSERT
511
+ AS
512
+ BEGIN
513
+ SET NOCOUNT ON;
514
+ UPDATE t
515
+ SET sys_created = ISNULL(i.sys_created, SYSDATETIME()),
516
+ sys_modified = SYSDATETIME(),
517
+ sys_modified_count = 0,
518
+ sys_dirty = ISNULL(i.sys_dirty, 0),
519
+ sys_table = '{table_name_sql}'
520
+ FROM {table_identifier} AS t
521
+ INNER JOIN inserted AS i ON t.sys_id = i.sys_id;
522
+ END;
523
+ """.strip(),
524
+ f"""
525
+ CREATE TRIGGER {schema_identifier}.{trigger_prefix}_update
526
+ ON {table_identifier}
527
+ AFTER UPDATE
528
+ AS
529
+ BEGIN
530
+ SET NOCOUNT ON;
531
+ UPDATE t
532
+ SET sys_created = d.sys_created,
533
+ sys_modified = SYSDATETIME(),
534
+ sys_table = '{table_name_sql}',
535
+ sys_dirty = CASE WHEN d.sys_dirty = 1 AND i.sys_dirty = 0 THEN 0 ELSE 1 END,
536
+ sys_modified_count = CASE WHEN d.sys_dirty = 1 AND i.sys_dirty = 0 THEN ISNULL(d.sys_modified_count, 0) ELSE ISNULL(d.sys_modified_count, 0) + 1 END
537
+ FROM {table_identifier} AS t
538
+ INNER JOIN inserted AS i ON t.sys_id = i.sys_id
539
+ INNER JOIN deleted AS d ON d.sys_id = i.sys_id;
540
+ END;
541
+ """.strip(),
542
+ ]
543
+
544
+ return "\n".join(statements), tuple()
391
545
 
392
546
  @classmethod
393
547
  def drop_table(cls, name):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.140
3
+ Version: 0.0.142
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
@@ -1,4 +1,4 @@
1
- velocity/__init__.py,sha256=4tuIcCyAF_BNn2pB2swolVQBfQPmUTvsDhNr9YkR7zM,147
1
+ velocity/__init__.py,sha256=SkDlRMRyhFqq5GpLQPd_olQYdhQHS2BT3Y4ioTVxupg,147
2
2
  velocity/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  velocity/app/invoices.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  velocity/app/orders.py,sha256=fr1oTBjSFfyeMBUXRG06LV4jgwrlwYNL5mbEBleFwf0,6328
@@ -36,7 +36,7 @@ velocity/db/core/engine.py,sha256=mNlaFPruHO935phKPVrsxZprGYUvxW-zp2sBcBZ-KCg,20
36
36
  velocity/db/core/result.py,sha256=b0ie3yZAOj9S57x32uFFGKZ95zhImmZ0iXl0X1qYszc,12813
37
37
  velocity/db/core/row.py,sha256=yqxm03uEDy3oSbnkCtKyiqFdSqG3zXTq2HIHYKOvPY4,7291
38
38
  velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
39
- velocity/db/core/table.py,sha256=EVoZZ6s21ZVqqT7VgH9Rr0SsFbsLLk6gtwvKwgbcWTQ,34708
39
+ velocity/db/core/table.py,sha256=zzyVZA8azaxB0QPxNjGFUOEwhk62emAZvHhkFlnACyY,35450
40
40
  velocity/db/core/transaction.py,sha256=unjmVkkfb7D8Wow6V8V8aLaxUZo316i--ksZxc4-I1Q,6613
41
41
  velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  velocity/db/servers/tablehelper.py,sha256=Q48ObN5KD_U2sBP0GUcjaQjKeE4Hr351sPQirwQ0_1s,22163
@@ -48,22 +48,22 @@ velocity/db/servers/base/types.py,sha256=3LBxFCD35eeIsIqftpAJh0JjUVonDYemz2n6AMt
48
48
  velocity/db/servers/mysql/__init__.py,sha256=mASO5JB0xkzYngwx2X79yyKifYRqxIdfKFWutIHuw7k,2661
49
49
  velocity/db/servers/mysql/operators.py,sha256=wHmVSPxlPGbOdvQEmsfKhD25H8djovSbNcmacLHDVkI,1273
50
50
  velocity/db/servers/mysql/reserved.py,sha256=s-aFMwYJpZ_1FBcCMU8fOdhml2ET58-59ZnUm7iw5OU,3312
51
- velocity/db/servers/mysql/sql.py,sha256=OdfpoOOeJip5_O2HRNSA7_gtiQfXgELGbrKpu_NP878,18115
51
+ velocity/db/servers/mysql/sql.py,sha256=_Ea84IJwILV4Mf70zM3r3PdFL2VHU2jWSCujFhiJlQM,22170
52
52
  velocity/db/servers/mysql/types.py,sha256=BMQf4TpsRo1JN-yOl1nSItTO-Juu2piSTNy5o_djBeM,3486
53
53
  velocity/db/servers/postgres/__init__.py,sha256=6YcTLXposmsrEaJgdUAM_QgD1TZDSILQrGcwWZ-dibk,2457
54
54
  velocity/db/servers/postgres/operators.py,sha256=y9k6enReeR5hJxU_lYYR2epoaw4qCxEqmYJJ5jjaVWA,1166
55
55
  velocity/db/servers/postgres/reserved.py,sha256=5tKLaqFV-HrWRj-nsrxl5KGbmeM3ukn_bPZK36XEu8M,3648
56
- velocity/db/servers/postgres/sql.py,sha256=tC-_kl2gYRZKvW5zxOxvdMv0ZXJrO4zpnjcXVuro9q0,47246
56
+ velocity/db/servers/postgres/sql.py,sha256=iZxvALFr3dpJm9BX_WE97E9RdM2RDCmi4M35HwXknVA,50141
57
57
  velocity/db/servers/postgres/types.py,sha256=W71x8iRx-IIJkQSjb29k-KGkqp-QS6SxB0BHYXd4k8w,6955
58
58
  velocity/db/servers/sqlite/__init__.py,sha256=EIx09YN1-Vm-4CXVcEf9DBgvd8FhIN9rEqIaSRrEcIk,2293
59
59
  velocity/db/servers/sqlite/operators.py,sha256=VzZgph8RrnHkIVqqWGqnJwcafgBzc_8ZQp-M8tMl-mw,1221
60
60
  velocity/db/servers/sqlite/reserved.py,sha256=4vOI06bjt8wg9KxdzDTF-iOd-ewY23NvSzthpdty2fA,1298
61
- velocity/db/servers/sqlite/sql.py,sha256=VvI2KBLivSyd42zYg221DVRjrSSOEDjXT9SM2zbFAEM,16701
61
+ velocity/db/servers/sqlite/sql.py,sha256=GD5m4oxvWTisfn-umnKtM0Qz8VUXTUnAqQWi9Uv2trQ,20908
62
62
  velocity/db/servers/sqlite/types.py,sha256=jpCJeV25x4Iytf6D6GXgK3hVYFAAFV4WKJC-d-m4kdU,3102
63
63
  velocity/db/servers/sqlserver/__init__.py,sha256=LN8OycN7W8da_ZPRYnPQ-O3Bv_xjret9qV1ZCitZlOU,2708
64
64
  velocity/db/servers/sqlserver/operators.py,sha256=xK8_doDLssS38SRs1NoAI7XTO0-gNGMDS76nTVru4kE,1104
65
65
  velocity/db/servers/sqlserver/reserved.py,sha256=Gn5n9DjxcjM-7PsIZPYigD6XLvMAYGnz1IrPuN7Dp2Y,2120
66
- velocity/db/servers/sqlserver/sql.py,sha256=b9UgFGma_FeY0S4VK9Yqn9QACS1SAZhJsEpA9muoHQI,20559
66
+ velocity/db/servers/sqlserver/sql.py,sha256=sdmFFzt_Np9mVRGPkFsnWfYPD8z88V9Sp5no3-maPHM,26208
67
67
  velocity/db/servers/sqlserver/types.py,sha256=FAODYEO137m-WugpM89f9bQN9q6S2cjjUaz0a9gfE6M,3745
68
68
  velocity/db/tests/__init__.py,sha256=7-hilWb43cKnSnCeXcjFG-6LpziN5k443IpsIvuevP0,24
69
69
  velocity/db/tests/common_db_test.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -121,8 +121,8 @@ velocity/misc/tests/test_merge.py,sha256=Vm5_jY5cVczw0hZF-3TYzmxFw81heJOJB-dvhCg
121
121
  velocity/misc/tests/test_oconv.py,sha256=fy4DwWGn_v486r2d_3ACpuBD-K1oOngNq1HJCGH7X-M,4694
122
122
  velocity/misc/tests/test_original_error.py,sha256=iWSd18tckOA54LoPQOGV5j9LAz2W-3_ZOwmyZ8-4YQc,1742
123
123
  velocity/misc/tests/test_timer.py,sha256=l9nrF84kHaFofvQYKInJmfoqC01wBhsUB18lVBgXCoo,2758
124
- velocity_python-0.0.140.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
125
- velocity_python-0.0.140.dist-info/METADATA,sha256=1ILF_tgyA45S-ax-Me6sC9D7Dqcb8zohfPe60SYyHEQ,34262
126
- velocity_python-0.0.140.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
127
- velocity_python-0.0.140.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
128
- velocity_python-0.0.140.dist-info/RECORD,,
124
+ velocity_python-0.0.142.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
125
+ velocity_python-0.0.142.dist-info/METADATA,sha256=lOnWDl2eeeGGfq1hv56p2mGXGz-LzGodk7wWdVpRoH0,34262
126
+ velocity_python-0.0.142.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
127
+ velocity_python-0.0.142.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
128
+ velocity_python-0.0.142.dist-info/RECORD,,