velocity-python 0.0.105__py3-none-any.whl → 0.0.155__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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +248 -0
- velocity/aws/handlers/context.py +167 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/__init__.py +16 -4
- velocity/db/core/decorators.py +20 -4
- velocity/db/core/engine.py +185 -792
- velocity/db/core/result.py +36 -22
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +283 -44
- velocity/db/core/transaction.py +19 -11
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +221 -0
- velocity/db/tests/test_postgres.py +448 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/db/utils.py +62 -47
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
- velocity_python-0.0.155.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.105.dist-info/RECORD +0 -56
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for database utility functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
from src.velocity.db.utils import (
|
|
8
|
+
safe_sort_key_none_last,
|
|
9
|
+
safe_sort_key_none_first,
|
|
10
|
+
safe_sort_key_with_default,
|
|
11
|
+
safe_sort_rows,
|
|
12
|
+
group_by_fields,
|
|
13
|
+
safe_sort_grouped_rows,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestDatabaseUtils(unittest.TestCase):
|
|
18
|
+
"""Test database utility functions."""
|
|
19
|
+
|
|
20
|
+
def setUp(self):
|
|
21
|
+
"""Set up test data."""
|
|
22
|
+
self.sample_data = [
|
|
23
|
+
{"id": 1, "name": "Alice", "date": "2024-03", "amount": 100},
|
|
24
|
+
{"id": 2, "name": "Bob", "date": None, "amount": 200},
|
|
25
|
+
{"id": 3, "name": "Charlie", "date": "2024-01", "amount": None},
|
|
26
|
+
{"id": 4, "name": "David", "date": "2024-02", "amount": 150},
|
|
27
|
+
{"id": 5, "name": "Eve", "date": None, "amount": 300},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def test_safe_sort_key_none_last(self):
|
|
31
|
+
"""Test sorting with None values at the end."""
|
|
32
|
+
sort_key = safe_sort_key_none_last("date")
|
|
33
|
+
sorted_data = sorted(self.sample_data, key=sort_key)
|
|
34
|
+
|
|
35
|
+
dates = [row["date"] for row in sorted_data]
|
|
36
|
+
expected = ["2024-01", "2024-02", "2024-03", None, None]
|
|
37
|
+
self.assertEqual(dates, expected)
|
|
38
|
+
|
|
39
|
+
def test_safe_sort_key_none_first(self):
|
|
40
|
+
"""Test sorting with None values at the beginning."""
|
|
41
|
+
sort_key = safe_sort_key_none_first("date")
|
|
42
|
+
sorted_data = sorted(self.sample_data, key=sort_key)
|
|
43
|
+
|
|
44
|
+
dates = [row["date"] for row in sorted_data]
|
|
45
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
46
|
+
self.assertEqual(dates, expected)
|
|
47
|
+
|
|
48
|
+
def test_safe_sort_key_with_default(self):
|
|
49
|
+
"""Test sorting with None values replaced by default."""
|
|
50
|
+
sort_key = safe_sort_key_with_default("date", "1900-01")
|
|
51
|
+
sorted_data = sorted(self.sample_data, key=sort_key)
|
|
52
|
+
|
|
53
|
+
dates = [row["date"] for row in sorted_data]
|
|
54
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
55
|
+
self.assertEqual(dates, expected)
|
|
56
|
+
|
|
57
|
+
def test_safe_sort_rows_none_last(self):
|
|
58
|
+
"""Test safe_sort_rows with none_handling='last'."""
|
|
59
|
+
sorted_data = safe_sort_rows(self.sample_data, "date", none_handling="last")
|
|
60
|
+
|
|
61
|
+
dates = [row["date"] for row in sorted_data]
|
|
62
|
+
expected = ["2024-01", "2024-02", "2024-03", None, None]
|
|
63
|
+
self.assertEqual(dates, expected)
|
|
64
|
+
|
|
65
|
+
def test_safe_sort_rows_none_first(self):
|
|
66
|
+
"""Test safe_sort_rows with none_handling='first'."""
|
|
67
|
+
sorted_data = safe_sort_rows(self.sample_data, "date", none_handling="first")
|
|
68
|
+
|
|
69
|
+
dates = [row["date"] for row in sorted_data]
|
|
70
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
71
|
+
self.assertEqual(dates, expected)
|
|
72
|
+
|
|
73
|
+
def test_safe_sort_rows_with_default(self):
|
|
74
|
+
"""Test safe_sort_rows with none_handling='default'."""
|
|
75
|
+
sorted_data = safe_sort_rows(
|
|
76
|
+
self.sample_data, "date", none_handling="default", default_value="1900-01"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
dates = [row["date"] for row in sorted_data]
|
|
80
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
81
|
+
self.assertEqual(dates, expected)
|
|
82
|
+
|
|
83
|
+
def test_safe_sort_rows_reverse(self):
|
|
84
|
+
"""Test safe_sort_rows with reverse=True."""
|
|
85
|
+
sorted_data = safe_sort_rows(self.sample_data, "date", reverse=True)
|
|
86
|
+
|
|
87
|
+
dates = [row["date"] for row in sorted_data]
|
|
88
|
+
expected = [None, None, "2024-03", "2024-02", "2024-01"]
|
|
89
|
+
self.assertEqual(dates, expected)
|
|
90
|
+
|
|
91
|
+
def test_safe_sort_rows_invalid_none_handling(self):
|
|
92
|
+
"""Test safe_sort_rows with invalid none_handling option."""
|
|
93
|
+
with self.assertRaises(ValueError) as context:
|
|
94
|
+
safe_sort_rows(self.sample_data, "date", none_handling="invalid")
|
|
95
|
+
|
|
96
|
+
self.assertIn("Invalid none_handling option", str(context.exception))
|
|
97
|
+
|
|
98
|
+
def test_group_by_fields_single_field(self):
|
|
99
|
+
"""Test grouping by a single field."""
|
|
100
|
+
# Add data with same names for grouping
|
|
101
|
+
test_data = [
|
|
102
|
+
{"name": "Alice", "type": "A", "value": 1},
|
|
103
|
+
{"name": "Bob", "type": "B", "value": 2},
|
|
104
|
+
{"name": "Alice", "type": "C", "value": 3},
|
|
105
|
+
{"name": "Bob", "type": "A", "value": 4},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
groups = group_by_fields(test_data, "name")
|
|
109
|
+
|
|
110
|
+
self.assertEqual(len(groups), 2)
|
|
111
|
+
self.assertIn(("Alice",), groups)
|
|
112
|
+
self.assertIn(("Bob",), groups)
|
|
113
|
+
self.assertEqual(len(groups[("Alice",)]), 2)
|
|
114
|
+
self.assertEqual(len(groups[("Bob",)]), 2)
|
|
115
|
+
|
|
116
|
+
def test_group_by_fields_multiple_fields(self):
|
|
117
|
+
"""Test grouping by multiple fields."""
|
|
118
|
+
test_data = [
|
|
119
|
+
{"name": "Alice", "type": "A", "value": 1},
|
|
120
|
+
{"name": "Bob", "type": "B", "value": 2},
|
|
121
|
+
{"name": "Alice", "type": "A", "value": 3},
|
|
122
|
+
{"name": "Alice", "type": "B", "value": 4},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
groups = group_by_fields(test_data, "name", "type")
|
|
126
|
+
|
|
127
|
+
self.assertEqual(len(groups), 3)
|
|
128
|
+
self.assertIn(("Alice", "A"), groups)
|
|
129
|
+
self.assertIn(("Bob", "B"), groups)
|
|
130
|
+
self.assertIn(("Alice", "B"), groups)
|
|
131
|
+
self.assertEqual(len(groups[("Alice", "A")]), 2)
|
|
132
|
+
self.assertEqual(len(groups[("Bob", "B")]), 1)
|
|
133
|
+
self.assertEqual(len(groups[("Alice", "B")]), 1)
|
|
134
|
+
|
|
135
|
+
def test_safe_sort_grouped_rows(self):
|
|
136
|
+
"""Test sorting rows within groups."""
|
|
137
|
+
# Create grouped data
|
|
138
|
+
test_data = [
|
|
139
|
+
{"group": "A", "date": "2024-03", "value": 1},
|
|
140
|
+
{"group": "A", "date": None, "value": 2},
|
|
141
|
+
{"group": "A", "date": "2024-01", "value": 3},
|
|
142
|
+
{"group": "B", "date": "2024-02", "value": 4},
|
|
143
|
+
{"group": "B", "date": None, "value": 5},
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
groups = group_by_fields(test_data, "group")
|
|
147
|
+
sorted_groups = safe_sort_grouped_rows(groups, "date")
|
|
148
|
+
|
|
149
|
+
# Check group A is sorted correctly
|
|
150
|
+
group_a_dates = [row["date"] for row in sorted_groups[("A",)]]
|
|
151
|
+
expected_a = ["2024-01", "2024-03", None]
|
|
152
|
+
self.assertEqual(group_a_dates, expected_a)
|
|
153
|
+
|
|
154
|
+
# Check group B is sorted correctly
|
|
155
|
+
group_b_dates = [row["date"] for row in sorted_groups[("B",)]]
|
|
156
|
+
expected_b = ["2024-02", None]
|
|
157
|
+
self.assertEqual(group_b_dates, expected_b)
|
|
158
|
+
|
|
159
|
+
def test_payment_profile_scenario(self):
|
|
160
|
+
"""Test the specific payment profile scenario that was failing."""
|
|
161
|
+
payment_profiles = [
|
|
162
|
+
{
|
|
163
|
+
"sys_id": 1,
|
|
164
|
+
"email_address": "test@example.com",
|
|
165
|
+
"card_number": "1234",
|
|
166
|
+
"expiration_date": "2024-12",
|
|
167
|
+
"status": "active",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"sys_id": 2,
|
|
171
|
+
"email_address": "test@example.com",
|
|
172
|
+
"card_number": "1234",
|
|
173
|
+
"expiration_date": "2024-06",
|
|
174
|
+
"status": "active",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"sys_id": 3,
|
|
178
|
+
"email_address": "test@example.com",
|
|
179
|
+
"card_number": "1234",
|
|
180
|
+
"expiration_date": None,
|
|
181
|
+
"status": "active",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"sys_id": 4,
|
|
185
|
+
"email_address": "other@example.com",
|
|
186
|
+
"card_number": "5678",
|
|
187
|
+
"expiration_date": "2025-01",
|
|
188
|
+
"status": "active",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"sys_id": 5,
|
|
192
|
+
"email_address": "other@example.com",
|
|
193
|
+
"card_number": "5678",
|
|
194
|
+
"expiration_date": None,
|
|
195
|
+
"status": "active",
|
|
196
|
+
},
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Group by email and card number
|
|
200
|
+
groups = group_by_fields(payment_profiles, "email_address", "card_number")
|
|
201
|
+
|
|
202
|
+
# Sort each group by expiration date
|
|
203
|
+
sorted_groups = safe_sort_grouped_rows(groups, "expiration_date")
|
|
204
|
+
|
|
205
|
+
# Verify we can safely enumerate through each group
|
|
206
|
+
for group_key, group in sorted_groups.items():
|
|
207
|
+
for idx, row in enumerate(group):
|
|
208
|
+
# This should not raise any errors
|
|
209
|
+
self.assertIsInstance(idx, int)
|
|
210
|
+
self.assertIn("sys_id", row)
|
|
211
|
+
self.assertIn("expiration_date", row)
|
|
212
|
+
|
|
213
|
+
# Check specific group sorting
|
|
214
|
+
test_group = sorted_groups[("test@example.com", "1234")]
|
|
215
|
+
exp_dates = [row["expiration_date"] for row in test_group]
|
|
216
|
+
expected = ["2024-06", "2024-12", None]
|
|
217
|
+
self.assertEqual(exp_dates, expected)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
unittest.main()
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import decimal
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest import mock
|
|
5
|
+
from velocity.db.servers.postgres.sql import SQL
|
|
6
|
+
from velocity.db.servers.tablehelper import TableHelper
|
|
7
|
+
from velocity.db.servers.postgres.types import TYPES
|
|
8
|
+
from velocity.db.core.table import Table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MockTx:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.table_cache = {}
|
|
14
|
+
self.cursor_cache = {}
|
|
15
|
+
|
|
16
|
+
def cursor(self):
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
def table(self, table_name):
|
|
20
|
+
# Return a mock table object
|
|
21
|
+
return MockTable()
|
|
22
|
+
|
|
23
|
+
class MockTable:
|
|
24
|
+
def column(self, column_name):
|
|
25
|
+
return MockColumn()
|
|
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
|
+
|
|
72
|
+
class MockColumn:
|
|
73
|
+
def __init__(self):
|
|
74
|
+
self.py_type = str
|
|
75
|
+
|
|
76
|
+
def exists(self):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
class TestSQLModule(unittest.TestCase):
|
|
80
|
+
def test_quote_simple_identifier(self):
|
|
81
|
+
self.assertEqual(TableHelper.quote("test"), "test")
|
|
82
|
+
|
|
83
|
+
def test_quote_reserved_word(self):
|
|
84
|
+
self.assertEqual(TableHelper.quote("SELECT"), '"SELECT"')
|
|
85
|
+
|
|
86
|
+
def test_quote_with_special_characters(self):
|
|
87
|
+
self.assertEqual(TableHelper.quote("my/schema"), '"my/schema"')
|
|
88
|
+
|
|
89
|
+
def test_quote_dot_notation(self):
|
|
90
|
+
self.assertEqual(TableHelper.quote("my_table.my_column"), "my_table.my_column")
|
|
91
|
+
|
|
92
|
+
def test_quote_list_identifiers(self):
|
|
93
|
+
self.assertEqual(
|
|
94
|
+
TableHelper.quote(["test", "SELECT", "my_table"]),
|
|
95
|
+
["test", '"SELECT"', "my_table"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def test_make_where_simple_equality(self):
|
|
99
|
+
# Create a mock transaction and table helper
|
|
100
|
+
mock_tx = MockTx()
|
|
101
|
+
helper = TableHelper(mock_tx, "test_table")
|
|
102
|
+
|
|
103
|
+
sql, vals = helper.make_where({"column1": "value1"})
|
|
104
|
+
self.assertIn("column1 = %s", sql)
|
|
105
|
+
self.assertEqual(vals, ("value1",))
|
|
106
|
+
|
|
107
|
+
def test_make_where_with_null(self):
|
|
108
|
+
mock_tx = MockTx()
|
|
109
|
+
helper = TableHelper(mock_tx, "test_table")
|
|
110
|
+
|
|
111
|
+
sql, vals = helper.make_where({"column1": None})
|
|
112
|
+
self.assertIn("column1 IS NULL", sql)
|
|
113
|
+
self.assertEqual(vals, ())
|
|
114
|
+
|
|
115
|
+
def test_make_where_with_not_null(self):
|
|
116
|
+
mock_tx = MockTx()
|
|
117
|
+
helper = TableHelper(mock_tx, "test_table")
|
|
118
|
+
|
|
119
|
+
sql, vals = helper.make_where({"column1!": None})
|
|
120
|
+
self.assertIn("column1! IS NULL", sql)
|
|
121
|
+
self.assertEqual(vals, ())
|
|
122
|
+
|
|
123
|
+
def test_make_where_with_operators(self):
|
|
124
|
+
mock_tx = MockTx()
|
|
125
|
+
helper = TableHelper(mock_tx, "test_table")
|
|
126
|
+
|
|
127
|
+
sql, vals = helper.make_where({"column1>": 10, "column2!": "value2"})
|
|
128
|
+
self.assertIn("column1> = %s", sql)
|
|
129
|
+
self.assertIn("column2! = %s", sql)
|
|
130
|
+
self.assertEqual(len(vals), 2)
|
|
131
|
+
|
|
132
|
+
def test_make_where_with_list(self):
|
|
133
|
+
mock_tx = MockTx()
|
|
134
|
+
helper = TableHelper(mock_tx, "test_table")
|
|
135
|
+
|
|
136
|
+
sql, vals = helper.make_where({"column1": [1, 2, 3]})
|
|
137
|
+
self.assertIn("column1 IN", sql)
|
|
138
|
+
self.assertEqual(len(vals), 3)
|
|
139
|
+
|
|
140
|
+
def test_make_where_between(self):
|
|
141
|
+
mock_tx = MockTx()
|
|
142
|
+
helper = TableHelper(mock_tx, "test_table")
|
|
143
|
+
|
|
144
|
+
sql, vals = helper.make_where({"column1><": [1, 10]})
|
|
145
|
+
self.assertIn("column1>< = %s", sql)
|
|
146
|
+
self.assertEqual(len(vals), 1) # Actual implementation returns one parameter
|
|
147
|
+
|
|
148
|
+
def test_sql_select_simple(self):
|
|
149
|
+
mock_tx = MockTx()
|
|
150
|
+
sql_query, params = SQL.select(mock_tx, columns="*", table="my_table")
|
|
151
|
+
self.assertIn("SELECT *", sql_query)
|
|
152
|
+
self.assertIn("FROM my_table", sql_query)
|
|
153
|
+
self.assertEqual(params, ())
|
|
154
|
+
|
|
155
|
+
def test_sql_select_with_where(self):
|
|
156
|
+
mock_tx = MockTx()
|
|
157
|
+
sql_query, params = SQL.select(mock_tx, columns="*", table="my_table", where={"id": 1})
|
|
158
|
+
self.assertIn("SELECT *", sql_query)
|
|
159
|
+
self.assertIn("WHERE id = %s", sql_query)
|
|
160
|
+
self.assertEqual(params, (1,))
|
|
161
|
+
|
|
162
|
+
def test_sql_select_with_order_by(self):
|
|
163
|
+
mock_tx = MockTx()
|
|
164
|
+
sql_query, params = SQL.select(mock_tx, columns="*", table="my_table", orderby="id DESC")
|
|
165
|
+
self.assertIn("SELECT *", sql_query)
|
|
166
|
+
self.assertIn("ORDER BY id DESC", sql_query)
|
|
167
|
+
self.assertEqual(params, ())
|
|
168
|
+
|
|
169
|
+
def test_sql_insert(self):
|
|
170
|
+
sql_query, params = SQL.insert(
|
|
171
|
+
table="my_table", data={"column1": "value1", "column2": 2}
|
|
172
|
+
)
|
|
173
|
+
self.assertIn("INSERT INTO my_table", sql_query)
|
|
174
|
+
self.assertIn("VALUES (%s,%s)", sql_query)
|
|
175
|
+
self.assertEqual(params, ("value1", 2))
|
|
176
|
+
|
|
177
|
+
def test_sql_update(self):
|
|
178
|
+
mock_tx = MockTx()
|
|
179
|
+
sql_query, params = SQL.update(
|
|
180
|
+
mock_tx, table="my_table", data={"column1": "new_value"}, pk={"id": 1}
|
|
181
|
+
)
|
|
182
|
+
self.assertIn("UPDATE my_table", sql_query)
|
|
183
|
+
self.assertIn("SET column1 = %s", sql_query)
|
|
184
|
+
self.assertIn("WHERE id = %s", sql_query)
|
|
185
|
+
self.assertEqual(params, ("new_value", 1))
|
|
186
|
+
|
|
187
|
+
def test_sql_delete(self):
|
|
188
|
+
mock_tx = MockTx()
|
|
189
|
+
sql_query, params = SQL.delete(mock_tx, table="my_table", where={"id": 1})
|
|
190
|
+
self.assertIn("DELETE", sql_query)
|
|
191
|
+
self.assertIn("FROM my_table", sql_query)
|
|
192
|
+
self.assertIn("WHERE id = %s", sql_query)
|
|
193
|
+
self.assertEqual(params, (1,))
|
|
194
|
+
|
|
195
|
+
def test_sql_create_table(self):
|
|
196
|
+
sql_query, params = SQL.create_table(
|
|
197
|
+
name="public.test_table", columns={"name": str, "age": int}, drop=True
|
|
198
|
+
)
|
|
199
|
+
self.assertIn("CREATE TABLE", sql_query)
|
|
200
|
+
self.assertIn("test_table", sql_query)
|
|
201
|
+
self.assertIn("DROP TABLE IF EXISTS", sql_query)
|
|
202
|
+
self.assertEqual(params, ())
|
|
203
|
+
|
|
204
|
+
def test_sql_drop_table(self):
|
|
205
|
+
sql_query, params = SQL.drop_table("public.test_table")
|
|
206
|
+
self.assertIn("drop table if exists", sql_query.lower())
|
|
207
|
+
self.assertIn("test_table", sql_query)
|
|
208
|
+
self.assertEqual(params, ())
|
|
209
|
+
|
|
210
|
+
def test_sql_create_index(self):
|
|
211
|
+
mock_tx = MockTx()
|
|
212
|
+
sql_query, params = SQL.create_index(
|
|
213
|
+
mock_tx, table="my_table", columns="column1", unique=True
|
|
214
|
+
)
|
|
215
|
+
self.assertIn("CREATE UNIQUE INDEX", sql_query)
|
|
216
|
+
self.assertIn("my_table", sql_query)
|
|
217
|
+
self.assertEqual(params, ())
|
|
218
|
+
|
|
219
|
+
def test_sql_drop_index(self):
|
|
220
|
+
sql_query, params = SQL.drop_index(table="my_table", columns="column1")
|
|
221
|
+
self.assertIn("DROP INDEX IF EXISTS", sql_query)
|
|
222
|
+
self.assertEqual(params, ())
|
|
223
|
+
|
|
224
|
+
def test_sql_foreign_key_creation(self):
|
|
225
|
+
sql_query, params = SQL.create_foreign_key(
|
|
226
|
+
table="child_table",
|
|
227
|
+
columns="parent_id",
|
|
228
|
+
key_to_table="parent_table",
|
|
229
|
+
key_to_columns="id",
|
|
230
|
+
)
|
|
231
|
+
self.assertIn("ALTER TABLE child_table ADD CONSTRAINT", sql_query)
|
|
232
|
+
self.assertIn(
|
|
233
|
+
"FOREIGN KEY (parent_id) REFERENCES parent_table (id);", sql_query
|
|
234
|
+
)
|
|
235
|
+
self.assertEqual(params, ())
|
|
236
|
+
|
|
237
|
+
def test_sql_merge_insert(self):
|
|
238
|
+
mock_tx = MockTx()
|
|
239
|
+
sql_query, params = SQL.merge(
|
|
240
|
+
mock_tx,
|
|
241
|
+
table="my_table",
|
|
242
|
+
data={"column1": "value1"},
|
|
243
|
+
pk={"id": 1},
|
|
244
|
+
on_conflict_do_nothing=True,
|
|
245
|
+
on_conflict_update=False,
|
|
246
|
+
)
|
|
247
|
+
self.assertIn("INSERT INTO my_table", sql_query)
|
|
248
|
+
self.assertIn("ON CONFLICT", sql_query)
|
|
249
|
+
self.assertIn("DO NOTHING", sql_query)
|
|
250
|
+
self.assertEqual(params, ("value1", 1))
|
|
251
|
+
|
|
252
|
+
def test_sql_merge_update(self):
|
|
253
|
+
mock_tx = MockTx()
|
|
254
|
+
sql_query, params = SQL.merge(
|
|
255
|
+
mock_tx,
|
|
256
|
+
table="my_table",
|
|
257
|
+
data={"column1": "value1"},
|
|
258
|
+
pk={"id": 1},
|
|
259
|
+
on_conflict_do_nothing=False,
|
|
260
|
+
on_conflict_update=True,
|
|
261
|
+
)
|
|
262
|
+
self.assertIn("INSERT INTO my_table", sql_query)
|
|
263
|
+
self.assertIn("ON CONFLICT", sql_query)
|
|
264
|
+
self.assertIn("DO", sql_query)
|
|
265
|
+
self.assertIn("UPDATE", sql_query)
|
|
266
|
+
self.assertIn("SET", sql_query)
|
|
267
|
+
self.assertEqual(params, ("value1", 1))
|
|
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
|
+
|
|
410
|
+
def test_get_type_mapping(self):
|
|
411
|
+
self.assertEqual(TYPES.get_type("string"), "TEXT")
|
|
412
|
+
self.assertEqual(TYPES.get_type(123), "BIGINT")
|
|
413
|
+
self.assertEqual(TYPES.get_type(123.456), "NUMERIC(19, 6)")
|
|
414
|
+
self.assertEqual(TYPES.get_type(True), "BOOLEAN")
|
|
415
|
+
self.assertEqual(TYPES.get_type(None), "TEXT")
|
|
416
|
+
|
|
417
|
+
def test_py_type_mapping(self):
|
|
418
|
+
self.assertEqual(TYPES.py_type("INTEGER"), int)
|
|
419
|
+
self.assertEqual(TYPES.py_type("NUMERIC"), decimal.Decimal)
|
|
420
|
+
self.assertEqual(TYPES.py_type("TEXT"), str)
|
|
421
|
+
self.assertEqual(TYPES.py_type("BOOLEAN"), bool)
|
|
422
|
+
|
|
423
|
+
def test_sql_truncate(self):
|
|
424
|
+
sql_query, params = SQL.truncate("my_table")
|
|
425
|
+
self.assertEqual(sql_query, "truncate table my_table")
|
|
426
|
+
self.assertEqual(params, ())
|
|
427
|
+
|
|
428
|
+
def test_sql_create_view(self):
|
|
429
|
+
sql_query, params = SQL.create_view(
|
|
430
|
+
name="my_view", query="SELECT * FROM my_table", temp=True, silent=True
|
|
431
|
+
)
|
|
432
|
+
self.assertIn("CREATE OR REPLACE", sql_query)
|
|
433
|
+
self.assertIn("TEMPORARY VIEW", sql_query)
|
|
434
|
+
self.assertIn("my_view", sql_query)
|
|
435
|
+
self.assertIn("SELECT *", sql_query)
|
|
436
|
+
self.assertIn("FROM my_table", sql_query)
|
|
437
|
+
self.assertEqual(params, ())
|
|
438
|
+
|
|
439
|
+
def test_sql_drop_view(self):
|
|
440
|
+
sql_query, params = SQL.drop_view(name="my_view", silent=True)
|
|
441
|
+
self.assertEqual(sql_query, "DROP VIEW IF EXISTS my_view")
|
|
442
|
+
self.assertEqual(params, ())
|
|
443
|
+
|
|
444
|
+
# Additional tests can be added here to cover more methods and edge cases
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
unittest.main()
|