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
|
@@ -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, has_column=False, has_row_column=False):
|
|
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,160 @@ 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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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(
|
|
489
|
+
cls, name, has_column=False, has_row_column=False
|
|
490
|
+
):
|
|
491
|
+
"""Ensure sys_modified_count exists for SQL Server tables along with maintenance triggers."""
|
|
492
|
+
if "." in name:
|
|
493
|
+
schema, table_name = name.split(".", 1)
|
|
494
|
+
else:
|
|
495
|
+
schema = cls.default_schema or "dbo"
|
|
496
|
+
table_name = name
|
|
497
|
+
|
|
498
|
+
schema_identifier = quote(schema)
|
|
499
|
+
table_identifier = quote(name if "." in name else f"{schema}.{table_name}")
|
|
500
|
+
object_name = f"[{schema}].[{table_name}]"
|
|
501
|
+
table_name_sql = table_name.replace("'", "''")
|
|
502
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"CC_SYS_MOD_{table_name}")
|
|
503
|
+
|
|
504
|
+
statements = []
|
|
505
|
+
if not has_column:
|
|
506
|
+
statements.append(
|
|
507
|
+
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;"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
statements.extend([
|
|
511
|
+
f"UPDATE {table_identifier} SET sys_modified_count = 0 WHERE sys_modified_count IS NULL;",
|
|
512
|
+
f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_insert', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_insert;",
|
|
513
|
+
f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_update', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_update;",
|
|
514
|
+
f"""
|
|
515
|
+
CREATE TRIGGER {schema_identifier}.{trigger_prefix}_insert
|
|
516
|
+
ON {table_identifier}
|
|
517
|
+
AFTER INSERT
|
|
518
|
+
AS
|
|
519
|
+
BEGIN
|
|
520
|
+
SET NOCOUNT ON;
|
|
521
|
+
UPDATE t
|
|
522
|
+
SET sys_created = ISNULL(i.sys_created, SYSDATETIME()),
|
|
523
|
+
sys_modified = SYSDATETIME(),
|
|
524
|
+
sys_modified_count = 0,
|
|
525
|
+
sys_dirty = ISNULL(i.sys_dirty, 0),
|
|
526
|
+
sys_table = '{table_name_sql}'
|
|
527
|
+
FROM {table_identifier} AS t
|
|
528
|
+
INNER JOIN inserted AS i ON t.sys_id = i.sys_id;
|
|
529
|
+
END;
|
|
530
|
+
""".strip(),
|
|
531
|
+
f"""
|
|
532
|
+
CREATE TRIGGER {schema_identifier}.{trigger_prefix}_update
|
|
533
|
+
ON {table_identifier}
|
|
534
|
+
AFTER UPDATE
|
|
535
|
+
AS
|
|
536
|
+
BEGIN
|
|
537
|
+
SET NOCOUNT ON;
|
|
538
|
+
UPDATE t
|
|
539
|
+
SET sys_created = d.sys_created,
|
|
540
|
+
sys_modified = SYSDATETIME(),
|
|
541
|
+
sys_table = '{table_name_sql}',
|
|
542
|
+
sys_dirty = CASE WHEN d.sys_dirty = 1 AND i.sys_dirty = 0 THEN 0 ELSE 1 END,
|
|
543
|
+
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
|
|
544
|
+
FROM {table_identifier} AS t
|
|
545
|
+
INNER JOIN inserted AS i ON t.sys_id = i.sys_id
|
|
546
|
+
INNER JOIN deleted AS d ON d.sys_id = i.sys_id;
|
|
547
|
+
END;
|
|
548
|
+
""".strip(),
|
|
549
|
+
])
|
|
550
|
+
|
|
551
|
+
return "\n".join(statements), tuple()
|
|
391
552
|
|
|
392
553
|
@classmethod
|
|
393
554
|
def drop_table(cls, name):
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
import decimal
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest import mock
|
|
3
5
|
from velocity.db.servers.postgres.sql import SQL
|
|
4
6
|
from velocity.db.servers.tablehelper import TableHelper
|
|
5
7
|
from velocity.db.servers.postgres.types import TYPES
|
|
8
|
+
from velocity.db.core.table import Table
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class MockTx:
|
|
@@ -21,6 +24,51 @@ class MockTable:
|
|
|
21
24
|
def column(self, column_name):
|
|
22
25
|
return MockColumn()
|
|
23
26
|
|
|
27
|
+
def primary_keys(self):
|
|
28
|
+
return ["id"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DummyCursor:
|
|
32
|
+
def __init__(self, rowcount=0):
|
|
33
|
+
self.rowcount = rowcount
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DummyResult:
|
|
37
|
+
def __init__(self, rowcount=0):
|
|
38
|
+
self.cursor = DummyCursor(rowcount)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DummyTx:
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.engine = SimpleNamespace(sql=SimpleNamespace(), schema_locked=False)
|
|
44
|
+
self.executed = []
|
|
45
|
+
self.next_results = []
|
|
46
|
+
|
|
47
|
+
def cursor(self):
|
|
48
|
+
return DummyCursor()
|
|
49
|
+
|
|
50
|
+
def create_savepoint(self, cursor=None):
|
|
51
|
+
sp_id = f"sp_{len(self.executed)}"
|
|
52
|
+
return sp_id
|
|
53
|
+
|
|
54
|
+
def release_savepoint(self, sp, cursor=None):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def rollback_savepoint(self, sp, cursor=None):
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def execute(self, sql, params, cursor=None):
|
|
61
|
+
self.executed.append((sql, params))
|
|
62
|
+
if self.next_results:
|
|
63
|
+
return self.next_results.pop(0)
|
|
64
|
+
return DummyResult(0)
|
|
65
|
+
|
|
66
|
+
def table(self, table_name):
|
|
67
|
+
return MockTable()
|
|
68
|
+
|
|
69
|
+
def primary_keys(self):
|
|
70
|
+
return ["id"]
|
|
71
|
+
|
|
24
72
|
class MockColumn:
|
|
25
73
|
def __init__(self):
|
|
26
74
|
self.py_type = str
|
|
@@ -218,6 +266,147 @@ class TestSQLModule(unittest.TestCase):
|
|
|
218
266
|
self.assertIn("SET", sql_query)
|
|
219
267
|
self.assertEqual(params, ("value1", 1))
|
|
220
268
|
|
|
269
|
+
def test_sql_insnx_with_explicit_where(self):
|
|
270
|
+
mock_tx = MockTx()
|
|
271
|
+
sql_query, params = SQL.insnx(
|
|
272
|
+
mock_tx,
|
|
273
|
+
table="my_table",
|
|
274
|
+
data={"id": 1, "column1": "value1"},
|
|
275
|
+
where={"column1": "value1"},
|
|
276
|
+
)
|
|
277
|
+
self.assertIn("INSERT INTO", sql_query)
|
|
278
|
+
self.assertIn("WHERE NOT EXISTS", sql_query)
|
|
279
|
+
self.assertIn("SELECT 1 FROM my_table", sql_query)
|
|
280
|
+
self.assertEqual(params, (1, "value1", "value1"))
|
|
281
|
+
|
|
282
|
+
def test_sql_insert_if_not_exists_alias(self):
|
|
283
|
+
mock_tx = MockTx()
|
|
284
|
+
sql_alias, params_alias = SQL.insert_if_not_exists(
|
|
285
|
+
mock_tx,
|
|
286
|
+
table="my_table",
|
|
287
|
+
data={"id": 1, "column1": "value1"},
|
|
288
|
+
where={"column1": "value1"},
|
|
289
|
+
)
|
|
290
|
+
sql_main, params_main = SQL.insnx(
|
|
291
|
+
mock_tx,
|
|
292
|
+
table="my_table",
|
|
293
|
+
data={"id": 1, "column1": "value1"},
|
|
294
|
+
where={"column1": "value1"},
|
|
295
|
+
)
|
|
296
|
+
self.assertEqual(sql_alias, sql_main)
|
|
297
|
+
self.assertEqual(params_alias, params_main)
|
|
298
|
+
|
|
299
|
+
def test_table_update_or_insert_updates_only(self):
|
|
300
|
+
tx = DummyTx()
|
|
301
|
+
table = Table(tx, "my_table")
|
|
302
|
+
table.cursor = mock.MagicMock(return_value=None)
|
|
303
|
+
table.update = mock.MagicMock(return_value=1)
|
|
304
|
+
ins_builder = mock.MagicMock()
|
|
305
|
+
table.sql = SimpleNamespace(insnx=ins_builder, insert_if_not_exists=ins_builder)
|
|
306
|
+
|
|
307
|
+
affected = table.update_or_insert(
|
|
308
|
+
update_data={"value": "new"},
|
|
309
|
+
insert_data={"id": 1, "value": "new"},
|
|
310
|
+
where={"id": 1},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
self.assertEqual(affected, 1)
|
|
314
|
+
table.update.assert_called_once()
|
|
315
|
+
ins_builder.assert_not_called()
|
|
316
|
+
|
|
317
|
+
def test_table_update_or_insert_falls_back_to_insert(self):
|
|
318
|
+
tx = DummyTx()
|
|
319
|
+
table = Table(tx, "my_table")
|
|
320
|
+
table.cursor = mock.MagicMock(return_value=None)
|
|
321
|
+
table.update = mock.MagicMock(return_value=0)
|
|
322
|
+
|
|
323
|
+
captured = {}
|
|
324
|
+
|
|
325
|
+
def fake_insnx(tx_ctx, table_name, data, where):
|
|
326
|
+
captured["tx"] = tx_ctx
|
|
327
|
+
captured["table"] = table_name
|
|
328
|
+
captured["data"] = dict(data)
|
|
329
|
+
captured["where"] = where
|
|
330
|
+
return ("INSERT", ("a", "b"))
|
|
331
|
+
|
|
332
|
+
ins_builder = mock.MagicMock(side_effect=fake_insnx)
|
|
333
|
+
table.sql = SimpleNamespace(insnx=ins_builder, insert_if_not_exists=ins_builder)
|
|
334
|
+
tx.next_results.append(DummyResult(1))
|
|
335
|
+
|
|
336
|
+
affected = table.update_or_insert(
|
|
337
|
+
update_data={"value": "new"},
|
|
338
|
+
where={"id": 1},
|
|
339
|
+
pk={"id": 1},
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
self.assertEqual(affected, 1)
|
|
343
|
+
table.update.assert_called_once()
|
|
344
|
+
ins_builder.assert_called_once()
|
|
345
|
+
self.assertEqual(captured["table"], "my_table")
|
|
346
|
+
self.assertEqual(captured["data"], {"value": "new", "id": 1})
|
|
347
|
+
self.assertEqual(captured["where"], {"id": 1})
|
|
348
|
+
|
|
349
|
+
def test_table_update_or_insert_sql_only(self):
|
|
350
|
+
tx = DummyTx()
|
|
351
|
+
table = Table(tx, "my_table")
|
|
352
|
+
table.cursor = mock.MagicMock(return_value=None)
|
|
353
|
+
table.update = mock.MagicMock(return_value=("UPDATE sql", ("u",)))
|
|
354
|
+
|
|
355
|
+
ins_builder = mock.MagicMock(return_value=("INSERT sql", ("i",)))
|
|
356
|
+
table.sql = SimpleNamespace(insnx=ins_builder, insert_if_not_exists=ins_builder)
|
|
357
|
+
|
|
358
|
+
result = table.update_or_insert(
|
|
359
|
+
update_data={"value": "new"},
|
|
360
|
+
where={"id": 1},
|
|
361
|
+
pk={"id": 1},
|
|
362
|
+
sql_only=True,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
self.assertEqual(result["update"], ("UPDATE sql", ("u",)))
|
|
366
|
+
self.assertEqual(result["insert"], ("INSERT sql", ("i",)))
|
|
367
|
+
table.update.assert_called_once_with({"value": "new"}, where={"id": 1}, pk={"id": 1}, sql_only=True)
|
|
368
|
+
ins_builder.assert_called_once()
|
|
369
|
+
|
|
370
|
+
def test_sql_merge_conflict_columns_are_quoted(self):
|
|
371
|
+
mock_tx = MockTx()
|
|
372
|
+
sql_query, _ = SQL.merge(
|
|
373
|
+
mock_tx,
|
|
374
|
+
table="my_table",
|
|
375
|
+
data={"payload": "value"},
|
|
376
|
+
pk={"select": 1},
|
|
377
|
+
on_conflict_do_nothing=False,
|
|
378
|
+
on_conflict_update=True,
|
|
379
|
+
)
|
|
380
|
+
self.assertIn('on conflict ("select")'.upper(), sql_query.upper())
|
|
381
|
+
|
|
382
|
+
def test_sql_merge_missing_auto_pk_values(self):
|
|
383
|
+
mock_tx = MockTx()
|
|
384
|
+
with self.assertRaisesRegex(
|
|
385
|
+
ValueError, "Primary key values missing from data for merge"
|
|
386
|
+
):
|
|
387
|
+
SQL.merge(
|
|
388
|
+
mock_tx,
|
|
389
|
+
table="my_table",
|
|
390
|
+
data={"column1": "value1"},
|
|
391
|
+
pk=None,
|
|
392
|
+
on_conflict_do_nothing=False,
|
|
393
|
+
on_conflict_update=True,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def test_sql_merge_auto_pk_without_update_columns_falls_back_to_do_nothing(self):
|
|
397
|
+
mock_tx = MockTx()
|
|
398
|
+
sql_query, params = SQL.merge(
|
|
399
|
+
mock_tx,
|
|
400
|
+
table="my_table",
|
|
401
|
+
data={"id": 1},
|
|
402
|
+
pk=None,
|
|
403
|
+
on_conflict_do_nothing=False,
|
|
404
|
+
on_conflict_update=True,
|
|
405
|
+
)
|
|
406
|
+
self.assertIn("DO NOTHING", sql_query)
|
|
407
|
+
self.assertNotIn(" DO UPDATE", sql_query)
|
|
408
|
+
self.assertEqual(params, (1,))
|
|
409
|
+
|
|
221
410
|
def test_get_type_mapping(self):
|
|
222
411
|
self.assertEqual(TYPES.get_type("string"), "TEXT")
|
|
223
412
|
self.assertEqual(TYPES.get_type(123), "BIGINT")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.152
|
|
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
|
|
@@ -497,7 +497,7 @@ def update_user(tx):
|
|
|
497
497
|
# Find and update using dictionary syntax
|
|
498
498
|
user = users.find(123) # Returns a row that behaves like a dict
|
|
499
499
|
user['name'] = 'Updated Name' # Direct assignment like a dict
|
|
500
|
-
user['
|
|
500
|
+
user['important_date'] = datetime.now() # No special methods needed
|
|
501
501
|
|
|
502
502
|
# Check if columns exist before updating
|
|
503
503
|
if 'phone' in user:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
velocity/__init__.py,sha256=
|
|
1
|
+
velocity/__init__.py,sha256=RyxrnzBhzGp5qsnb1PU0iJPVNsvSTg5ZIlnrnL5P3m4,147
|
|
2
2
|
velocity/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
velocity/app/invoices.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
velocity/app/orders.py,sha256=
|
|
4
|
+
velocity/app/orders.py,sha256=C7ewngMpO8nD3ul_82o4FhZBdRkWvJtnuEbEJUKDCno,6151
|
|
5
5
|
velocity/app/payments.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
velocity/app/purchase_orders.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
velocity/app/tests/__init__.py,sha256=mqbNes8CjWTYLgCEgmu3EZudF6HZj671friAsAa4m_E,19
|
|
@@ -18,7 +18,8 @@ velocity/aws/handlers/lambda_handler.py,sha256=0wa_CHyJOaI5RsHqB0Ae83-B-SwlKR0qk
|
|
|
18
18
|
velocity/aws/handlers/response.py,sha256=s2Kw7yv5zAir1mEmfv6yBVIvRcRQ__xyryf1SrvtiRc,9317
|
|
19
19
|
velocity/aws/handlers/sqs_handler.py,sha256=azuV8DrFOh0hM13EnPzyYVBS-3fLe2fn9OPc4ho7sGc,3375
|
|
20
20
|
velocity/aws/handlers/mixins/__init__.py,sha256=_zyEpsnKikF7D7X-F0GA4cyIrQ6wBq7k5j6Vhp17vaQ,623
|
|
21
|
-
velocity/aws/handlers/mixins/activity_tracker.py,sha256=
|
|
21
|
+
velocity/aws/handlers/mixins/activity_tracker.py,sha256=vyQ_8kpSprjzLoALDv7g2rVkfstn89Tbsg6Zb9GmVOk,6579
|
|
22
|
+
velocity/aws/handlers/mixins/aws_session_mixin.py,sha256=yTa2-n4zgv23wbW3uZUp-L4CUJy8vSL8IMMNjMlYFVg,6806
|
|
22
23
|
velocity/aws/handlers/mixins/error_handler.py,sha256=uN2YF9v-3LzS3o_HdVpO-XMcPy3sS7SHjUg_LfbsG7Q,6803
|
|
23
24
|
velocity/aws/handlers/mixins/legacy_mixin.py,sha256=_YhiPU-zzXQjGNSAKhoUwfTFlnczmU-3BkwNFqr0hYE,2117
|
|
24
25
|
velocity/aws/handlers/mixins/standard_mixin.py,sha256=-wBX0PFlZAnxQsaMDEWr-xmU8TcRbQ4BZD3wmAKR2d0,2489
|
|
@@ -34,10 +35,10 @@ velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,
|
|
|
34
35
|
velocity/db/core/decorators.py,sha256=quhjMoEmK_l2jF7jXyL5Fgv8uisIpBz34Au5d3U6UHs,5276
|
|
35
36
|
velocity/db/core/engine.py,sha256=mNlaFPruHO935phKPVrsxZprGYUvxW-zp2sBcBZ-KCg,20666
|
|
36
37
|
velocity/db/core/result.py,sha256=b0ie3yZAOj9S57x32uFFGKZ95zhImmZ0iXl0X1qYszc,12813
|
|
37
|
-
velocity/db/core/row.py,sha256=
|
|
38
|
+
velocity/db/core/row.py,sha256=GOWm-HEBPCBwdqMHMBRc41m0Hoht4vRVQLkvdogX1fU,7729
|
|
38
39
|
velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
|
|
39
|
-
velocity/db/core/table.py,sha256=
|
|
40
|
-
velocity/db/core/transaction.py,sha256=
|
|
40
|
+
velocity/db/core/table.py,sha256=GyD4quWUGKXOiyZyE_bSESVscSGiCCQOVtixf7snfZ0,41940
|
|
41
|
+
velocity/db/core/transaction.py,sha256=VbB6GSdTT1Puy_j1tQnx9Ia3L3GZZFWaGw4xYWzpKAg,6733
|
|
41
42
|
velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
43
|
velocity/db/servers/tablehelper.py,sha256=Q48ObN5KD_U2sBP0GUcjaQjKeE4Hr351sPQirwQ0_1s,22163
|
|
43
44
|
velocity/db/servers/base/__init__.py,sha256=5--XJUeEAm7O6Ns2C_ODCr5TjFhdAge-zApZCT0LGTQ,285
|
|
@@ -48,28 +49,28 @@ velocity/db/servers/base/types.py,sha256=3LBxFCD35eeIsIqftpAJh0JjUVonDYemz2n6AMt
|
|
|
48
49
|
velocity/db/servers/mysql/__init__.py,sha256=mASO5JB0xkzYngwx2X79yyKifYRqxIdfKFWutIHuw7k,2661
|
|
49
50
|
velocity/db/servers/mysql/operators.py,sha256=wHmVSPxlPGbOdvQEmsfKhD25H8djovSbNcmacLHDVkI,1273
|
|
50
51
|
velocity/db/servers/mysql/reserved.py,sha256=s-aFMwYJpZ_1FBcCMU8fOdhml2ET58-59ZnUm7iw5OU,3312
|
|
51
|
-
velocity/db/servers/mysql/sql.py,sha256=
|
|
52
|
+
velocity/db/servers/mysql/sql.py,sha256=CxskGe86I-8idLNZmcG7IPqlc-BQM9cpJU6WS5KdCA0,22210
|
|
52
53
|
velocity/db/servers/mysql/types.py,sha256=BMQf4TpsRo1JN-yOl1nSItTO-Juu2piSTNy5o_djBeM,3486
|
|
53
54
|
velocity/db/servers/postgres/__init__.py,sha256=6YcTLXposmsrEaJgdUAM_QgD1TZDSILQrGcwWZ-dibk,2457
|
|
54
55
|
velocity/db/servers/postgres/operators.py,sha256=y9k6enReeR5hJxU_lYYR2epoaw4qCxEqmYJJ5jjaVWA,1166
|
|
55
56
|
velocity/db/servers/postgres/reserved.py,sha256=5tKLaqFV-HrWRj-nsrxl5KGbmeM3ukn_bPZK36XEu8M,3648
|
|
56
|
-
velocity/db/servers/postgres/sql.py,sha256=
|
|
57
|
+
velocity/db/servers/postgres/sql.py,sha256=oF0Bll75sTOrRQhBNo3dklRpUFhLixil4i09eVC9Y8Y,54450
|
|
57
58
|
velocity/db/servers/postgres/types.py,sha256=W71x8iRx-IIJkQSjb29k-KGkqp-QS6SxB0BHYXd4k8w,6955
|
|
58
59
|
velocity/db/servers/sqlite/__init__.py,sha256=EIx09YN1-Vm-4CXVcEf9DBgvd8FhIN9rEqIaSRrEcIk,2293
|
|
59
60
|
velocity/db/servers/sqlite/operators.py,sha256=VzZgph8RrnHkIVqqWGqnJwcafgBzc_8ZQp-M8tMl-mw,1221
|
|
60
61
|
velocity/db/servers/sqlite/reserved.py,sha256=4vOI06bjt8wg9KxdzDTF-iOd-ewY23NvSzthpdty2fA,1298
|
|
61
|
-
velocity/db/servers/sqlite/sql.py,sha256=
|
|
62
|
+
velocity/db/servers/sqlite/sql.py,sha256=iAENHbN8mfVsQHoqnEppynVMP_PdqXJX8jZQDNzr0ro,20948
|
|
62
63
|
velocity/db/servers/sqlite/types.py,sha256=jpCJeV25x4Iytf6D6GXgK3hVYFAAFV4WKJC-d-m4kdU,3102
|
|
63
64
|
velocity/db/servers/sqlserver/__init__.py,sha256=LN8OycN7W8da_ZPRYnPQ-O3Bv_xjret9qV1ZCitZlOU,2708
|
|
64
65
|
velocity/db/servers/sqlserver/operators.py,sha256=xK8_doDLssS38SRs1NoAI7XTO0-gNGMDS76nTVru4kE,1104
|
|
65
66
|
velocity/db/servers/sqlserver/reserved.py,sha256=Gn5n9DjxcjM-7PsIZPYigD6XLvMAYGnz1IrPuN7Dp2Y,2120
|
|
66
|
-
velocity/db/servers/sqlserver/sql.py,sha256=
|
|
67
|
+
velocity/db/servers/sqlserver/sql.py,sha256=h4fnVuNWaQE2c2sEEkLSIlBGf3xZP-lDtwILhF2-g3c,26368
|
|
67
68
|
velocity/db/servers/sqlserver/types.py,sha256=FAODYEO137m-WugpM89f9bQN9q6S2cjjUaz0a9gfE6M,3745
|
|
68
69
|
velocity/db/tests/__init__.py,sha256=7-hilWb43cKnSnCeXcjFG-6LpziN5k443IpsIvuevP0,24
|
|
69
70
|
velocity/db/tests/common_db_test.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
71
|
velocity/db/tests/test_cursor_rowcount_fix.py,sha256=mZRL1SBb9Knh67CSFyvfwj_LAarE_ilfVwpQHW18Yy8,5507
|
|
71
72
|
velocity/db/tests/test_db_utils.py,sha256=mSbEQXYKpWidX1FEnjrmt3q3K4ra0YTtQclrS46ufEE,8426
|
|
72
|
-
velocity/db/tests/test_postgres.py,sha256=
|
|
73
|
+
velocity/db/tests/test_postgres.py,sha256=NoBydNkGmXn8olXwva4C4sYV3cKzERd6Df0wHixxoyE,15554
|
|
73
74
|
velocity/db/tests/test_postgres_unchanged.py,sha256=rNcy7S_HXazi_MjU8QjRZO4q8dULMeG4tg6eN-rPPz8,2998
|
|
74
75
|
velocity/db/tests/test_process_error_robustness.py,sha256=CZr_co_o6PK7dejOr_gwdn0iKTzjWPTY5k-PwJ6oh9s,11361
|
|
75
76
|
velocity/db/tests/test_result_caching.py,sha256=DgsGXWL4G79MZOslCjq_t8qtdhCcXkHjQqV5zsF6i6M,8960
|
|
@@ -121,8 +122,8 @@ velocity/misc/tests/test_merge.py,sha256=Vm5_jY5cVczw0hZF-3TYzmxFw81heJOJB-dvhCg
|
|
|
121
122
|
velocity/misc/tests/test_oconv.py,sha256=fy4DwWGn_v486r2d_3ACpuBD-K1oOngNq1HJCGH7X-M,4694
|
|
122
123
|
velocity/misc/tests/test_original_error.py,sha256=iWSd18tckOA54LoPQOGV5j9LAz2W-3_ZOwmyZ8-4YQc,1742
|
|
123
124
|
velocity/misc/tests/test_timer.py,sha256=l9nrF84kHaFofvQYKInJmfoqC01wBhsUB18lVBgXCoo,2758
|
|
124
|
-
velocity_python-0.0.
|
|
125
|
-
velocity_python-0.0.
|
|
126
|
-
velocity_python-0.0.
|
|
127
|
-
velocity_python-0.0.
|
|
128
|
-
velocity_python-0.0.
|
|
125
|
+
velocity_python-0.0.152.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
|
|
126
|
+
velocity_python-0.0.152.dist-info/METADATA,sha256=eMxsOZlWyZS4ziuZ2_P0T-vC6vr5lcBEXRHA8Yk8w_M,34266
|
|
127
|
+
velocity_python-0.0.152.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
128
|
+
velocity_python-0.0.152.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
|
|
129
|
+
velocity_python-0.0.152.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|