velocity-python 0.0.138__py3-none-any.whl → 0.0.152__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 +1 -1
- velocity/app/orders.py +1 -3
- velocity/aws/handlers/mixins/activity_tracker.py +53 -14
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/db/core/row.py +12 -2
- velocity/db/core/table.py +178 -0
- velocity/db/core/transaction.py +6 -10
- velocity/db/servers/mysql/sql.py +132 -5
- velocity/db/servers/postgres/sql.py +254 -79
- velocity/db/servers/sqlite/sql.py +127 -4
- velocity/db/servers/sqlserver/sql.py +165 -4
- velocity/db/tests/test_postgres.py +189 -0
- {velocity_python-0.0.138.dist-info → velocity_python-0.0.152.dist-info}/METADATA +2 -2
- {velocity_python-0.0.138.dist-info → velocity_python-0.0.152.dist-info}/RECORD +17 -16
- {velocity_python-0.0.138.dist-info → velocity_python-0.0.152.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.138.dist-info → velocity_python-0.0.152.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.138.dist-info → velocity_python-0.0.152.dist-info}/top_level.txt +0 -0
velocity/db/servers/mysql/sql.py
CHANGED
|
@@ -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, has_column=False, has_row_column=False):
|
|
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):
|
|
@@ -18,39 +18,6 @@ TableHelper.reserved = reserved_words
|
|
|
18
18
|
TableHelper.operators = OPERATORS
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def _get_table_helper(tx, table):
|
|
22
|
-
"""
|
|
23
|
-
Utility function to create a TableHelper instance.
|
|
24
|
-
Ensures consistent configuration across all SQL methods.
|
|
25
|
-
"""
|
|
26
|
-
return TableHelper(tx, table)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _validate_table_name(table):
|
|
30
|
-
"""Validate table name format."""
|
|
31
|
-
if not table or not isinstance(table, str):
|
|
32
|
-
raise ValueError("Table name must be a non-empty string")
|
|
33
|
-
# Add more validation as needed
|
|
34
|
-
return table.strip()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _handle_predicate_errors(predicates, operation="WHERE"):
|
|
38
|
-
"""Process a list of predicates with error handling."""
|
|
39
|
-
sql_parts = []
|
|
40
|
-
vals = []
|
|
41
|
-
|
|
42
|
-
for pred, val in predicates:
|
|
43
|
-
sql_parts.append(pred)
|
|
44
|
-
if val is None:
|
|
45
|
-
pass
|
|
46
|
-
elif isinstance(val, tuple):
|
|
47
|
-
vals.extend(val)
|
|
48
|
-
else:
|
|
49
|
-
vals.append(val)
|
|
50
|
-
|
|
51
|
-
return sql_parts, vals
|
|
52
|
-
|
|
53
|
-
|
|
54
21
|
system_fields = [
|
|
55
22
|
"sys_id",
|
|
56
23
|
"sys_created",
|
|
@@ -143,7 +110,7 @@ class SQL(BaseSQLDialect):
|
|
|
143
110
|
vals = []
|
|
144
111
|
|
|
145
112
|
# Create table helper instance
|
|
146
|
-
th =
|
|
113
|
+
th = TableHelper(tx, table)
|
|
147
114
|
|
|
148
115
|
# Handle columns and DISTINCT before aliasing
|
|
149
116
|
if columns is None:
|
|
@@ -447,7 +414,7 @@ class SQL(BaseSQLDialect):
|
|
|
447
414
|
if not isinstance(data, Mapping) or not data:
|
|
448
415
|
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
449
416
|
|
|
450
|
-
th =
|
|
417
|
+
th = TableHelper(tx, table)
|
|
451
418
|
set_clauses = []
|
|
452
419
|
vals = []
|
|
453
420
|
|
|
@@ -595,51 +562,164 @@ class SQL(BaseSQLDialect):
|
|
|
595
562
|
|
|
596
563
|
@classmethod
|
|
597
564
|
def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
|
|
565
|
+
if not isinstance(data, Mapping) or not data:
|
|
566
|
+
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
567
|
+
|
|
568
|
+
table_helper = TableHelper(tx, table)
|
|
569
|
+
data = dict(data) # work with a copy to avoid mutating the caller's dict
|
|
570
|
+
|
|
598
571
|
if pk is None:
|
|
599
572
|
pkeys = tx.table(table).primary_keys()
|
|
600
573
|
if not pkeys:
|
|
601
574
|
raise ValueError("Primary key required for merge.")
|
|
602
|
-
|
|
603
|
-
if
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
575
|
+
missing = [key for key in pkeys if key not in data]
|
|
576
|
+
if missing:
|
|
577
|
+
missing_cols = ", ".join(missing)
|
|
578
|
+
raise ValueError(
|
|
579
|
+
"Primary key values missing from data for merge: "
|
|
580
|
+
f"{missing_cols}. Provide pk=... or include the key values in data."
|
|
581
|
+
)
|
|
582
|
+
pk = {key: data[key] for key in pkeys}
|
|
583
|
+
else:
|
|
584
|
+
pk = dict(pk)
|
|
585
|
+
for key, value in pk.items():
|
|
586
|
+
if key in data and data[key] != value:
|
|
587
|
+
raise ValueError(
|
|
588
|
+
f"Conflicting values for primary key '{key}' between data and pk arguments."
|
|
589
|
+
)
|
|
609
590
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
full_data.update(data)
|
|
613
|
-
full_data.update(pk)
|
|
591
|
+
insert_data = dict(data)
|
|
592
|
+
insert_data.update(pk)
|
|
614
593
|
|
|
615
|
-
|
|
616
|
-
sql = [sql]
|
|
617
|
-
vals = list(vals) # Convert to a mutable list
|
|
594
|
+
update_data = {k: v for k, v in data.items() if k not in pk}
|
|
618
595
|
|
|
619
|
-
if
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if on_conflict_do_nothing:
|
|
626
|
-
sql.append("NOTHING")
|
|
627
|
-
elif on_conflict_update:
|
|
628
|
-
# Call update() with excluded=True to produce the SET clause for the upsert.
|
|
629
|
-
sql_update, vals_update = cls.update(tx, table, data, pk, excluded=True)
|
|
630
|
-
sql.append(sql_update)
|
|
631
|
-
# Use list.extend to add the update values to vals.
|
|
632
|
-
vals.extend(vals_update)
|
|
633
|
-
else:
|
|
596
|
+
if not update_data and on_conflict_update:
|
|
597
|
+
# Nothing to update, fall back to a no-op on conflict resolution.
|
|
598
|
+
on_conflict_do_nothing = True
|
|
599
|
+
on_conflict_update = False
|
|
600
|
+
|
|
601
|
+
if on_conflict_do_nothing == on_conflict_update:
|
|
634
602
|
raise Exception(
|
|
635
603
|
"Update on conflict must have one and only one option to complete on conflict."
|
|
636
604
|
)
|
|
637
605
|
|
|
606
|
+
sql, vals = cls.insert(table, insert_data)
|
|
607
|
+
sql = [sql]
|
|
608
|
+
vals = list(vals) # Convert to a mutable list
|
|
609
|
+
|
|
610
|
+
sql.append("ON CONFLICT")
|
|
611
|
+
conflict_columns = [TableHelper.quote(column) for column in pk.keys()]
|
|
612
|
+
sql.append("(")
|
|
613
|
+
sql.append(", ".join(conflict_columns))
|
|
614
|
+
sql.append(")")
|
|
615
|
+
sql.append("DO")
|
|
616
|
+
if on_conflict_do_nothing:
|
|
617
|
+
sql.append("NOTHING")
|
|
618
|
+
elif on_conflict_update:
|
|
619
|
+
sql_update, vals_update = cls.update(
|
|
620
|
+
tx, table, update_data, pk, excluded=True
|
|
621
|
+
)
|
|
622
|
+
sql.append(sql_update)
|
|
623
|
+
vals.extend(vals_update)
|
|
624
|
+
|
|
638
625
|
import sqlparse
|
|
639
626
|
|
|
640
627
|
final_sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
641
628
|
return final_sql, tuple(vals)
|
|
642
629
|
|
|
630
|
+
@classmethod
|
|
631
|
+
def insnx(cls, tx, table, data, where=None):
|
|
632
|
+
"""Insert only when the supplied predicate finds no existing row."""
|
|
633
|
+
if not table:
|
|
634
|
+
raise ValueError("Table name is required.")
|
|
635
|
+
if not isinstance(data, Mapping) or not data:
|
|
636
|
+
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
637
|
+
|
|
638
|
+
# Helper used for quoting and foreign key resolution
|
|
639
|
+
th = TableHelper(tx, table)
|
|
640
|
+
quote_helper = TableHelper(None, table)
|
|
641
|
+
|
|
642
|
+
columns_sql = []
|
|
643
|
+
select_parts = []
|
|
644
|
+
vals = []
|
|
645
|
+
|
|
646
|
+
for key, val in data.items():
|
|
647
|
+
columns_sql.append(quote_helper.quote(key.lower()))
|
|
648
|
+
if isinstance(val, str) and len(val) > 2 and val.startswith("@@") and val[2:]:
|
|
649
|
+
select_parts.append(val[2:])
|
|
650
|
+
else:
|
|
651
|
+
select_parts.append("%s")
|
|
652
|
+
vals.append(val)
|
|
653
|
+
|
|
654
|
+
if not select_parts:
|
|
655
|
+
raise ValueError("At least one column is required for insert.")
|
|
656
|
+
|
|
657
|
+
if where is None:
|
|
658
|
+
if tx is None:
|
|
659
|
+
raise ValueError(
|
|
660
|
+
"A transaction context is required when deriving WHERE from primary keys."
|
|
661
|
+
)
|
|
662
|
+
pk_cols = tx.table(table).primary_keys()
|
|
663
|
+
if not pk_cols:
|
|
664
|
+
raise ValueError("Primary key required to derive WHERE clause.")
|
|
665
|
+
missing = [pk for pk in pk_cols if pk not in data]
|
|
666
|
+
if missing:
|
|
667
|
+
raise ValueError(
|
|
668
|
+
"Missing primary key value(s) for insert condition: " + ", ".join(missing)
|
|
669
|
+
)
|
|
670
|
+
where = {pk: data[pk] for pk in pk_cols}
|
|
671
|
+
|
|
672
|
+
where_clauses = []
|
|
673
|
+
where_vals = []
|
|
674
|
+
|
|
675
|
+
if isinstance(where, Mapping):
|
|
676
|
+
compiled = []
|
|
677
|
+
for key, val in where.items():
|
|
678
|
+
compiled.append(th.make_predicate(key, val))
|
|
679
|
+
where = compiled
|
|
680
|
+
|
|
681
|
+
if isinstance(where, str):
|
|
682
|
+
where_clauses.append(where)
|
|
683
|
+
else:
|
|
684
|
+
try:
|
|
685
|
+
for predicate, value in where:
|
|
686
|
+
where_clauses.append(predicate)
|
|
687
|
+
if value is None:
|
|
688
|
+
continue
|
|
689
|
+
if isinstance(value, tuple):
|
|
690
|
+
where_vals.extend(value)
|
|
691
|
+
else:
|
|
692
|
+
where_vals.append(value)
|
|
693
|
+
except (TypeError, ValueError) as exc:
|
|
694
|
+
raise ValueError(
|
|
695
|
+
"Invalid WHERE clause format. Expected mapping, SQL string, or iterable of predicate/value pairs."
|
|
696
|
+
) from exc
|
|
697
|
+
|
|
698
|
+
vals.extend(where_vals)
|
|
699
|
+
|
|
700
|
+
exists_sql = [
|
|
701
|
+
"SELECT 1 FROM",
|
|
702
|
+
TableHelper.quote(table),
|
|
703
|
+
]
|
|
704
|
+
if where_clauses:
|
|
705
|
+
exists_sql.append("WHERE " + " AND ".join(where_clauses))
|
|
706
|
+
|
|
707
|
+
sql_parts = [
|
|
708
|
+
"INSERT INTO",
|
|
709
|
+
TableHelper.quote(table),
|
|
710
|
+
f"({','.join(columns_sql)})",
|
|
711
|
+
"SELECT",
|
|
712
|
+
", ".join(select_parts),
|
|
713
|
+
"WHERE NOT EXISTS (",
|
|
714
|
+
" ".join(exists_sql),
|
|
715
|
+
")",
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
final_sql = sqlparse.format(" ".join(sql_parts), reindent=True, keyword_case="upper")
|
|
719
|
+
return final_sql, tuple(vals)
|
|
720
|
+
|
|
721
|
+
insert_if_not_exists = insnx
|
|
722
|
+
|
|
643
723
|
@classmethod
|
|
644
724
|
def version(cls):
|
|
645
725
|
return "select version()", tuple()
|
|
@@ -734,11 +814,13 @@ class SQL(BaseSQLDialect):
|
|
|
734
814
|
f"""
|
|
735
815
|
CREATE TABLE {fqtn} (
|
|
736
816
|
sys_id BIGSERIAL PRIMARY KEY,
|
|
737
|
-
sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
738
817
|
sys_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
739
|
-
|
|
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,
|
|
740
822
|
sys_dirty BOOLEAN NOT NULL DEFAULT FALSE,
|
|
741
|
-
sys_table TEXT,
|
|
823
|
+
sys_table TEXT NOT NULL,
|
|
742
824
|
description TEXT
|
|
743
825
|
);
|
|
744
826
|
|
|
@@ -747,19 +829,30 @@ class SQL(BaseSQLDialect):
|
|
|
747
829
|
CREATE OR REPLACE FUNCTION {schema}.on_sys_modified()
|
|
748
830
|
RETURNS TRIGGER AS
|
|
749
831
|
$BODY$
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
832
|
+
BEGIN
|
|
833
|
+
IF (TG_OP = 'INSERT') THEN
|
|
834
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
835
|
+
NEW.sys_created := transaction_timestamp();
|
|
836
|
+
NEW.sys_modified := transaction_timestamp();
|
|
837
|
+
NEW.sys_modified_row := clock_timestamp();
|
|
838
|
+
NEW.sys_modified_count := 0;
|
|
839
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
840
|
+
NEW.sys_table := TG_TABLE_NAME;
|
|
841
|
+
NEW.sys_created := OLD.sys_created;
|
|
842
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0);
|
|
843
|
+
IF ROW(NEW.*) IS DISTINCT FROM ROW(OLD.*) THEN
|
|
844
|
+
IF OLD.sys_dirty IS TRUE AND NEW.sys_dirty IS FALSE THEN
|
|
845
|
+
NEW.sys_dirty := FALSE;
|
|
846
|
+
ELSE
|
|
847
|
+
NEW.sys_dirty := TRUE;
|
|
758
848
|
END IF;
|
|
759
|
-
|
|
760
|
-
NEW.
|
|
761
|
-
|
|
762
|
-
|
|
849
|
+
NEW.sys_modified := transaction_timestamp();
|
|
850
|
+
NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
|
|
851
|
+
END IF;
|
|
852
|
+
END IF;
|
|
853
|
+
|
|
854
|
+
RETURN NEW;
|
|
855
|
+
END;
|
|
763
856
|
$BODY$
|
|
764
857
|
LANGUAGE plpgsql VOLATILE
|
|
765
858
|
COST 100;
|
|
@@ -782,6 +875,88 @@ class SQL(BaseSQLDialect):
|
|
|
782
875
|
sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
783
876
|
return sql, tuple()
|
|
784
877
|
|
|
878
|
+
@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."""
|
|
883
|
+
if "." in name:
|
|
884
|
+
schema_name, table_name = name.split(".", 1)
|
|
885
|
+
else:
|
|
886
|
+
schema_name = cls.default_schema
|
|
887
|
+
table_name = name
|
|
888
|
+
|
|
889
|
+
schema_identifier = TableHelper.quote(schema_name)
|
|
890
|
+
table_identifier = TableHelper.quote(table_name)
|
|
891
|
+
fqtn = f"{schema_identifier}.{table_identifier}"
|
|
892
|
+
|
|
893
|
+
trigger_name = (
|
|
894
|
+
f"on_update_row_{schema_name}_{table_name}"
|
|
895
|
+
.replace(".", "_")
|
|
896
|
+
.replace('"', "")
|
|
897
|
+
)
|
|
898
|
+
trigger_identifier = TableHelper.quote(trigger_name)
|
|
899
|
+
column_name = TableHelper.quote("sys_modified_count")
|
|
900
|
+
row_column_name = TableHelper.quote("sys_modified_row")
|
|
901
|
+
|
|
902
|
+
statements = []
|
|
903
|
+
if not has_column:
|
|
904
|
+
statements.append(
|
|
905
|
+
f"ALTER TABLE {fqtn} ADD COLUMN {column_name} INTEGER NOT NULL DEFAULT 0;"
|
|
906
|
+
)
|
|
907
|
+
if not has_row_column:
|
|
908
|
+
statements.append(
|
|
909
|
+
f"ALTER TABLE {fqtn} ADD COLUMN {row_column_name} TIMESTAMPTZ;"
|
|
910
|
+
)
|
|
911
|
+
|
|
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
|
+
])
|
|
954
|
+
|
|
955
|
+
sql = sqlparse.format(
|
|
956
|
+
" ".join(statements), reindent=True, keyword_case="upper"
|
|
957
|
+
)
|
|
958
|
+
return sql, tuple()
|
|
959
|
+
|
|
785
960
|
@classmethod
|
|
786
961
|
def drop_table(cls, name):
|
|
787
962
|
return f"drop table if exists {TableHelper.quote(name)} cascade;", tuple()
|