velocity-python 0.0.109__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 -839
- velocity/db/core/result.py +30 -24
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +279 -40
- 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.109.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.109.dist-info/RECORD +0 -56
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import hashlib
|
|
3
|
+
import decimal
|
|
4
|
+
import datetime
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from velocity.db import exceptions
|
|
9
|
+
from ..base.sql import BaseSQLDialect
|
|
10
|
+
from .reserved import reserved_words
|
|
11
|
+
from .types import TYPES
|
|
12
|
+
from .operators import OPERATORS, SQLiteOperators
|
|
13
|
+
from ..tablehelper import TableHelper
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Configure TableHelper for SQLite
|
|
17
|
+
TableHelper.reserved = reserved_words
|
|
18
|
+
TableHelper.operators = OPERATORS
|
|
19
|
+
|
|
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
|
+
|
|
33
|
+
def quote(data):
|
|
34
|
+
"""Quote SQLite identifiers."""
|
|
35
|
+
if isinstance(data, list):
|
|
36
|
+
return [quote(item) for item in data]
|
|
37
|
+
else:
|
|
38
|
+
parts = data.split(".")
|
|
39
|
+
new = []
|
|
40
|
+
for part in parts:
|
|
41
|
+
if '"' in part:
|
|
42
|
+
new.append(part)
|
|
43
|
+
elif part.upper() in reserved_words:
|
|
44
|
+
new.append('"' + part + '"')
|
|
45
|
+
elif re.findall("[/]", part):
|
|
46
|
+
new.append('"' + part + '"')
|
|
47
|
+
else:
|
|
48
|
+
new.append(part)
|
|
49
|
+
return ".".join(new)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SQL(BaseSQLDialect):
|
|
53
|
+
server = "SQLite3"
|
|
54
|
+
type_column_identifier = "type"
|
|
55
|
+
is_nullable = "notnull"
|
|
56
|
+
|
|
57
|
+
default_schema = ""
|
|
58
|
+
|
|
59
|
+
# SQLite error codes (numeric)
|
|
60
|
+
ApplicationErrorCodes = []
|
|
61
|
+
DatabaseMissingErrorCodes = [] # SQLite creates databases on demand
|
|
62
|
+
TableMissingErrorCodes = [] # Detected by error message
|
|
63
|
+
ColumnMissingErrorCodes = [] # Detected by error message
|
|
64
|
+
ForeignKeyMissingErrorCodes = []
|
|
65
|
+
ConnectionErrorCodes = []
|
|
66
|
+
DuplicateKeyErrorCodes = [] # Detected by error message
|
|
67
|
+
RetryTransactionCodes = [] # SQLITE_BUSY
|
|
68
|
+
TruncationErrorCodes = []
|
|
69
|
+
LockTimeoutErrorCodes = [] # SQLITE_BUSY
|
|
70
|
+
DatabaseObjectExistsErrorCodes = []
|
|
71
|
+
DataIntegrityErrorCodes = []
|
|
72
|
+
|
|
73
|
+
types = TYPES
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def get_error(cls, e):
|
|
77
|
+
"""Extract error information from SQLite exception."""
|
|
78
|
+
# SQLite exceptions don't have error codes like other databases
|
|
79
|
+
return None, str(e)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def select(
|
|
83
|
+
cls,
|
|
84
|
+
tx,
|
|
85
|
+
columns=None,
|
|
86
|
+
table=None,
|
|
87
|
+
where=None,
|
|
88
|
+
orderby=None,
|
|
89
|
+
groupby=None,
|
|
90
|
+
having=None,
|
|
91
|
+
start=None,
|
|
92
|
+
qty=None,
|
|
93
|
+
lock=None,
|
|
94
|
+
skip_locked=None,
|
|
95
|
+
):
|
|
96
|
+
"""Generate a SQLite SELECT statement."""
|
|
97
|
+
if not table:
|
|
98
|
+
raise ValueError("Table name is required")
|
|
99
|
+
|
|
100
|
+
sql_parts = []
|
|
101
|
+
vals = []
|
|
102
|
+
|
|
103
|
+
# SELECT clause
|
|
104
|
+
if columns is None:
|
|
105
|
+
columns = ["*"]
|
|
106
|
+
elif isinstance(columns, str):
|
|
107
|
+
columns = [columns]
|
|
108
|
+
|
|
109
|
+
sql_parts.append("SELECT")
|
|
110
|
+
sql_parts.append(", ".join(columns))
|
|
111
|
+
|
|
112
|
+
# FROM clause
|
|
113
|
+
sql_parts.append("FROM")
|
|
114
|
+
sql_parts.append(quote(table))
|
|
115
|
+
|
|
116
|
+
# WHERE clause
|
|
117
|
+
if where:
|
|
118
|
+
where_sql, where_vals = cls._build_where(where)
|
|
119
|
+
sql_parts.append("WHERE")
|
|
120
|
+
sql_parts.append(where_sql)
|
|
121
|
+
vals.extend(where_vals)
|
|
122
|
+
|
|
123
|
+
# GROUP BY clause
|
|
124
|
+
if groupby:
|
|
125
|
+
if isinstance(groupby, str):
|
|
126
|
+
groupby = [groupby]
|
|
127
|
+
sql_parts.append("GROUP BY")
|
|
128
|
+
sql_parts.append(", ".join(quote(col) for col in groupby))
|
|
129
|
+
|
|
130
|
+
# HAVING clause
|
|
131
|
+
if having:
|
|
132
|
+
having_sql, having_vals = cls._build_where(having)
|
|
133
|
+
sql_parts.append("HAVING")
|
|
134
|
+
sql_parts.append(having_sql)
|
|
135
|
+
vals.extend(having_vals)
|
|
136
|
+
|
|
137
|
+
# ORDER BY clause
|
|
138
|
+
if orderby:
|
|
139
|
+
if isinstance(orderby, str):
|
|
140
|
+
orderby = [orderby]
|
|
141
|
+
elif isinstance(orderby, dict):
|
|
142
|
+
orderby_list = []
|
|
143
|
+
for col, direction in orderby.items():
|
|
144
|
+
orderby_list.append(f"{quote(col)} {direction.upper()}")
|
|
145
|
+
orderby = orderby_list
|
|
146
|
+
sql_parts.append("ORDER BY")
|
|
147
|
+
sql_parts.append(", ".join(orderby))
|
|
148
|
+
|
|
149
|
+
# LIMIT and OFFSET (SQLite syntax)
|
|
150
|
+
if qty is not None:
|
|
151
|
+
sql_parts.append(f"LIMIT {qty}")
|
|
152
|
+
if start is not None:
|
|
153
|
+
sql_parts.append(f"OFFSET {start}")
|
|
154
|
+
|
|
155
|
+
# Note: SQLite doesn't support row-level locking like FOR UPDATE
|
|
156
|
+
if lock:
|
|
157
|
+
pass # Ignored for SQLite
|
|
158
|
+
|
|
159
|
+
return " ".join(sql_parts), vals
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def _build_where(cls, where):
|
|
163
|
+
"""Build WHERE clause for SQLite."""
|
|
164
|
+
if isinstance(where, str):
|
|
165
|
+
return where, []
|
|
166
|
+
|
|
167
|
+
if isinstance(where, dict):
|
|
168
|
+
where = list(where.items())
|
|
169
|
+
|
|
170
|
+
if not isinstance(where, (list, tuple)):
|
|
171
|
+
raise ValueError("WHERE clause must be string, dict, or list")
|
|
172
|
+
|
|
173
|
+
conditions = []
|
|
174
|
+
vals = []
|
|
175
|
+
|
|
176
|
+
for key, val in where:
|
|
177
|
+
if val is None:
|
|
178
|
+
if "!" in key:
|
|
179
|
+
key = key.replace("!", "")
|
|
180
|
+
conditions.append(f"{quote(key)} IS NOT NULL")
|
|
181
|
+
else:
|
|
182
|
+
conditions.append(f"{quote(key)} IS NULL")
|
|
183
|
+
elif isinstance(val, (list, tuple)):
|
|
184
|
+
if "!" in key:
|
|
185
|
+
key = key.replace("!", "")
|
|
186
|
+
conditions.append(f"{quote(key)} NOT IN ({', '.join(['?'] * len(val))})")
|
|
187
|
+
else:
|
|
188
|
+
conditions.append(f"{quote(key)} IN ({', '.join(['?'] * len(val))})")
|
|
189
|
+
vals.extend(val)
|
|
190
|
+
else:
|
|
191
|
+
# Handle operators
|
|
192
|
+
op = "="
|
|
193
|
+
if "<>" in key:
|
|
194
|
+
key = key.replace("<>", "")
|
|
195
|
+
op = "<>"
|
|
196
|
+
elif "!=" in key:
|
|
197
|
+
key = key.replace("!=", "")
|
|
198
|
+
op = "<>"
|
|
199
|
+
elif "%" in key:
|
|
200
|
+
key = key.replace("%", "")
|
|
201
|
+
op = "LIKE"
|
|
202
|
+
elif "!" in key:
|
|
203
|
+
key = key.replace("!", "")
|
|
204
|
+
op = "<>"
|
|
205
|
+
|
|
206
|
+
conditions.append(f"{quote(key)} {op} ?")
|
|
207
|
+
vals.append(val)
|
|
208
|
+
|
|
209
|
+
return " AND ".join(conditions), vals
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def insert(cls, table, data):
|
|
213
|
+
"""Generate an INSERT statement for SQLite."""
|
|
214
|
+
if not data:
|
|
215
|
+
raise ValueError("Data cannot be empty")
|
|
216
|
+
|
|
217
|
+
columns = list(data.keys())
|
|
218
|
+
values = list(data.values())
|
|
219
|
+
|
|
220
|
+
sql_parts = [
|
|
221
|
+
"INSERT INTO",
|
|
222
|
+
quote(table),
|
|
223
|
+
f"({', '.join(quote(col) for col in columns)})",
|
|
224
|
+
"VALUES",
|
|
225
|
+
f"({', '.join(['?'] * len(values))})" # SQLite uses ? placeholders
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
return " ".join(sql_parts), values
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def update(cls, tx, table, data, where=None, pk=None, excluded=False):
|
|
232
|
+
"""Generate an UPDATE statement for SQLite."""
|
|
233
|
+
if not data:
|
|
234
|
+
raise ValueError("Data cannot be empty")
|
|
235
|
+
|
|
236
|
+
if not where and not pk:
|
|
237
|
+
raise ValueError("Either WHERE clause or primary key must be provided")
|
|
238
|
+
|
|
239
|
+
# Build SET clause
|
|
240
|
+
set_clauses = []
|
|
241
|
+
vals = []
|
|
242
|
+
|
|
243
|
+
for col, val in data.items():
|
|
244
|
+
set_clauses.append(f"{quote(col)} = ?")
|
|
245
|
+
vals.append(val)
|
|
246
|
+
|
|
247
|
+
# Build WHERE clause
|
|
248
|
+
if pk:
|
|
249
|
+
if where:
|
|
250
|
+
# Merge pk into where
|
|
251
|
+
if isinstance(where, dict):
|
|
252
|
+
where.update(pk)
|
|
253
|
+
else:
|
|
254
|
+
# Convert to dict for merging
|
|
255
|
+
where_dict = dict(where) if isinstance(where, (list, tuple)) else {}
|
|
256
|
+
where_dict.update(pk)
|
|
257
|
+
where = where_dict
|
|
258
|
+
else:
|
|
259
|
+
where = pk
|
|
260
|
+
|
|
261
|
+
where_sql, where_vals = cls._build_where(where) if where else ("", [])
|
|
262
|
+
|
|
263
|
+
sql_parts = [
|
|
264
|
+
"UPDATE",
|
|
265
|
+
quote(table),
|
|
266
|
+
"SET",
|
|
267
|
+
", ".join(set_clauses)
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
if where_sql:
|
|
271
|
+
sql_parts.extend(["WHERE", where_sql])
|
|
272
|
+
vals.extend(where_vals)
|
|
273
|
+
|
|
274
|
+
return " ".join(sql_parts), vals
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def delete(cls, tx, table, where):
|
|
278
|
+
"""Generate a DELETE statement for SQLite."""
|
|
279
|
+
if not where:
|
|
280
|
+
raise ValueError("WHERE clause is required for DELETE")
|
|
281
|
+
|
|
282
|
+
where_sql, where_vals = cls._build_where(where)
|
|
283
|
+
|
|
284
|
+
sql_parts = [
|
|
285
|
+
"DELETE FROM",
|
|
286
|
+
quote(table),
|
|
287
|
+
"WHERE",
|
|
288
|
+
where_sql
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
return " ".join(sql_parts), where_vals
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
|
|
295
|
+
"""Generate an INSERT OR REPLACE/INSERT OR IGNORE statement for SQLite."""
|
|
296
|
+
if on_conflict_do_nothing:
|
|
297
|
+
# SQLite: INSERT OR IGNORE
|
|
298
|
+
insert_sql, insert_vals = cls.insert(table, data)
|
|
299
|
+
insert_sql = insert_sql.replace("INSERT INTO", "INSERT OR IGNORE INTO")
|
|
300
|
+
return insert_sql, insert_vals
|
|
301
|
+
elif on_conflict_update:
|
|
302
|
+
# SQLite: INSERT OR REPLACE (simple replacement)
|
|
303
|
+
insert_sql, insert_vals = cls.insert(table, data)
|
|
304
|
+
insert_sql = insert_sql.replace("INSERT INTO", "INSERT OR REPLACE INTO")
|
|
305
|
+
return insert_sql, insert_vals
|
|
306
|
+
else:
|
|
307
|
+
return cls.insert(table, data)
|
|
308
|
+
|
|
309
|
+
# Metadata queries
|
|
310
|
+
@classmethod
|
|
311
|
+
def version(cls):
|
|
312
|
+
return "SELECT sqlite_version()"
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def timestamp(cls):
|
|
316
|
+
return "SELECT datetime('now')"
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def user(cls):
|
|
320
|
+
return "SELECT 'sqlite_user'" # SQLite doesn't have users
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def databases(cls):
|
|
324
|
+
return "PRAGMA database_list"
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def schemas(cls):
|
|
328
|
+
return "PRAGMA database_list"
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def current_schema(cls):
|
|
332
|
+
return "SELECT 'main'" # SQLite default schema
|
|
333
|
+
|
|
334
|
+
@classmethod
|
|
335
|
+
def current_database(cls):
|
|
336
|
+
return "SELECT 'main'"
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def tables(cls, system=False):
|
|
340
|
+
if system:
|
|
341
|
+
return "SELECT name FROM sqlite_master WHERE type='table'"
|
|
342
|
+
else:
|
|
343
|
+
return "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def views(cls, system=False):
|
|
347
|
+
return "SELECT name FROM sqlite_master WHERE type='view'"
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def create_database(cls, name):
|
|
351
|
+
return f"-- SQLite databases are files: {name}"
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def drop_database(cls, name):
|
|
355
|
+
return f"-- SQLite databases are files: {name}"
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
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 = []
|
|
369
|
+
if drop:
|
|
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_system_columns(cls, name, existing_columns=None, force=False):
|
|
435
|
+
"""Ensure SQLite tables maintain the Velocity system triggers/columns."""
|
|
436
|
+
existing_columns = {col.lower() for col in existing_columns or []}
|
|
437
|
+
|
|
438
|
+
table_identifier = quote(name)
|
|
439
|
+
base_name = name.split(".")[-1].replace('"', "")
|
|
440
|
+
base_name_sql = base_name.replace("'", "''")
|
|
441
|
+
trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"cc_sysmod_{base_name}")
|
|
442
|
+
|
|
443
|
+
has_count = "sys_modified_count" in existing_columns
|
|
444
|
+
|
|
445
|
+
add_column = not has_count
|
|
446
|
+
recreate_triggers = force or add_column
|
|
447
|
+
|
|
448
|
+
if not recreate_triggers and not force:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
statements = []
|
|
452
|
+
|
|
453
|
+
if add_column:
|
|
454
|
+
statements.append(
|
|
455
|
+
f"ALTER TABLE {table_identifier} ADD COLUMN sys_modified_count INTEGER NOT NULL DEFAULT 0;"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
statements.append(
|
|
459
|
+
f"UPDATE {table_identifier} SET sys_modified_count = 0 WHERE sys_modified_count IS NULL;"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_ai;")
|
|
463
|
+
statements.append(f"DROP TRIGGER IF EXISTS {trigger_prefix}_au;")
|
|
464
|
+
|
|
465
|
+
statements.extend(
|
|
466
|
+
[
|
|
467
|
+
f"""
|
|
468
|
+
CREATE TRIGGER {trigger_prefix}_ai
|
|
469
|
+
AFTER INSERT ON {table_identifier}
|
|
470
|
+
FOR EACH ROW
|
|
471
|
+
BEGIN
|
|
472
|
+
UPDATE {table_identifier}
|
|
473
|
+
SET sys_created = COALESCE(NEW.sys_created, CURRENT_TIMESTAMP),
|
|
474
|
+
sys_modified = CURRENT_TIMESTAMP,
|
|
475
|
+
sys_modified_count = 0,
|
|
476
|
+
sys_dirty = COALESCE(NEW.sys_dirty, 0),
|
|
477
|
+
sys_table = '{base_name_sql}'
|
|
478
|
+
WHERE rowid = NEW.rowid;
|
|
479
|
+
END;
|
|
480
|
+
""".strip(),
|
|
481
|
+
f"""
|
|
482
|
+
CREATE TRIGGER {trigger_prefix}_au
|
|
483
|
+
AFTER UPDATE ON {table_identifier}
|
|
484
|
+
FOR EACH ROW
|
|
485
|
+
BEGIN
|
|
486
|
+
UPDATE {table_identifier}
|
|
487
|
+
SET sys_created = OLD.sys_created,
|
|
488
|
+
sys_modified = CURRENT_TIMESTAMP,
|
|
489
|
+
sys_table = '{base_name_sql}',
|
|
490
|
+
sys_dirty = CASE WHEN OLD.sys_dirty = 1 AND NEW.sys_dirty = 0 THEN 0 ELSE 1 END,
|
|
491
|
+
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
|
|
492
|
+
WHERE rowid = NEW.rowid;
|
|
493
|
+
END;
|
|
494
|
+
""".strip(),
|
|
495
|
+
]
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return "\n".join(statements), tuple()
|
|
499
|
+
|
|
500
|
+
@classmethod
|
|
501
|
+
def drop_table(cls, name):
|
|
502
|
+
return f"DROP TABLE {quote(name)}"
|
|
503
|
+
|
|
504
|
+
@classmethod
|
|
505
|
+
def truncate(cls, table):
|
|
506
|
+
return f"DELETE FROM {quote(table)}" # SQLite doesn't have TRUNCATE
|
|
507
|
+
|
|
508
|
+
@classmethod
|
|
509
|
+
def columns(cls, name):
|
|
510
|
+
return f"PRAGMA table_info({quote(name)})"
|
|
511
|
+
|
|
512
|
+
@classmethod
|
|
513
|
+
def column_info(cls, table, name):
|
|
514
|
+
return f"PRAGMA table_info({quote(table)})"
|
|
515
|
+
|
|
516
|
+
@classmethod
|
|
517
|
+
def drop_column(cls, table, name, cascade=True):
|
|
518
|
+
# SQLite doesn't support DROP COLUMN directly
|
|
519
|
+
return f"-- SQLite doesn't support DROP COLUMN for {table}.{name}"
|
|
520
|
+
|
|
521
|
+
@classmethod
|
|
522
|
+
def alter_add(cls, table, columns, null_allowed=True):
|
|
523
|
+
alter_parts = []
|
|
524
|
+
for col, col_type in columns.items():
|
|
525
|
+
null_clause = "" if null_allowed else " NOT NULL"
|
|
526
|
+
alter_parts.append(f"ALTER TABLE {quote(table)} ADD COLUMN {quote(col)} {col_type}{null_clause}")
|
|
527
|
+
|
|
528
|
+
return "; ".join(alter_parts)
|
|
529
|
+
|
|
530
|
+
@classmethod
|
|
531
|
+
def alter_drop(cls, table, columns):
|
|
532
|
+
return f"-- SQLite doesn't support DROP COLUMN for {table}"
|
|
533
|
+
|
|
534
|
+
@classmethod
|
|
535
|
+
def alter_column_by_type(cls, table, column, value, nullable=True):
|
|
536
|
+
return f"-- SQLite doesn't support ALTER COLUMN for {table}.{column}"
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def alter_column_by_sql(cls, table, column, value):
|
|
540
|
+
return f"-- SQLite doesn't support ALTER COLUMN for {table}.{column}"
|
|
541
|
+
|
|
542
|
+
@classmethod
|
|
543
|
+
def rename_column(cls, table, orig, new):
|
|
544
|
+
return f"ALTER TABLE {quote(table)} RENAME COLUMN {quote(orig)} TO {quote(new)}"
|
|
545
|
+
|
|
546
|
+
@classmethod
|
|
547
|
+
def rename_table(cls, table, new):
|
|
548
|
+
return f"ALTER TABLE {quote(table)} RENAME TO {quote(new)}"
|
|
549
|
+
|
|
550
|
+
@classmethod
|
|
551
|
+
def primary_keys(cls, table):
|
|
552
|
+
return f"PRAGMA table_info({quote(table)})"
|
|
553
|
+
|
|
554
|
+
@classmethod
|
|
555
|
+
def foreign_key_info(cls, table=None, column=None, schema=None):
|
|
556
|
+
if table:
|
|
557
|
+
return f"PRAGMA foreign_key_list({quote(table)})"
|
|
558
|
+
else:
|
|
559
|
+
return "-- SQLite foreign key info requires table name"
|
|
560
|
+
|
|
561
|
+
@classmethod
|
|
562
|
+
def create_foreign_key(cls, table, columns, key_to_table, key_to_columns, name=None, schema=None):
|
|
563
|
+
# SQLite foreign keys must be defined at table creation time
|
|
564
|
+
return f"-- SQLite foreign keys must be defined at table creation"
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def drop_foreign_key(cls, table, columns, key_to_table=None, key_to_columns=None, name=None, schema=None):
|
|
568
|
+
return f"-- SQLite foreign keys must be dropped by recreating table"
|
|
569
|
+
|
|
570
|
+
@classmethod
|
|
571
|
+
def create_index(cls, tx, table=None, columns=None, unique=False, direction=None, where=None, name=None, schema=None, trigram=None, lower=None):
|
|
572
|
+
if name is None:
|
|
573
|
+
name = f"idx_{table}_{'_'.join(columns)}"
|
|
574
|
+
|
|
575
|
+
index_type = "UNIQUE INDEX" if unique else "INDEX"
|
|
576
|
+
col_list = ", ".join(quote(col) for col in columns)
|
|
577
|
+
|
|
578
|
+
sql = f"CREATE {index_type} {quote(name)} ON {quote(table)} ({col_list})"
|
|
579
|
+
|
|
580
|
+
if where:
|
|
581
|
+
sql += f" WHERE {where}"
|
|
582
|
+
|
|
583
|
+
return sql
|
|
584
|
+
|
|
585
|
+
@classmethod
|
|
586
|
+
def drop_index(cls, table=None, columns=None, name=None, schema=None, trigram=None):
|
|
587
|
+
if name is None:
|
|
588
|
+
name = f"idx_{table}_{'_'.join(columns)}"
|
|
589
|
+
|
|
590
|
+
return f"DROP INDEX {quote(name)}"
|
|
591
|
+
|
|
592
|
+
@classmethod
|
|
593
|
+
def indexes(cls, table):
|
|
594
|
+
return f"PRAGMA index_list({quote(table)})"
|
|
595
|
+
|
|
596
|
+
@classmethod
|
|
597
|
+
def create_savepoint(cls, sp):
|
|
598
|
+
return f"SAVEPOINT {sp}"
|
|
599
|
+
|
|
600
|
+
@classmethod
|
|
601
|
+
def release_savepoint(cls, sp):
|
|
602
|
+
return f"RELEASE SAVEPOINT {sp}"
|
|
603
|
+
|
|
604
|
+
@classmethod
|
|
605
|
+
def rollback_savepoint(cls, sp):
|
|
606
|
+
return f"ROLLBACK TO SAVEPOINT {sp}"
|
|
607
|
+
|
|
608
|
+
@classmethod
|
|
609
|
+
def create_view(cls, name, query, temp=False, silent=True):
|
|
610
|
+
temp_clause = "TEMPORARY " if temp else ""
|
|
611
|
+
return f"CREATE {temp_clause}VIEW {quote(name)} AS {query}"
|
|
612
|
+
|
|
613
|
+
@classmethod
|
|
614
|
+
def drop_view(cls, name, silent=True):
|
|
615
|
+
if silent:
|
|
616
|
+
return f"DROP VIEW IF EXISTS {quote(name)}"
|
|
617
|
+
else:
|
|
618
|
+
return f"DROP VIEW {quote(name)}"
|
|
619
|
+
|
|
620
|
+
@classmethod
|
|
621
|
+
def last_id(cls, table):
|
|
622
|
+
return "SELECT last_insert_rowid()"
|
|
623
|
+
|
|
624
|
+
@classmethod
|
|
625
|
+
def current_id(cls, table):
|
|
626
|
+
return f"SELECT seq FROM sqlite_sequence WHERE name = '{table}'"
|
|
627
|
+
|
|
628
|
+
@classmethod
|
|
629
|
+
def set_id(cls, table, start):
|
|
630
|
+
return f"UPDATE sqlite_sequence SET seq = {start} WHERE name = '{table}'"
|
|
631
|
+
|
|
632
|
+
@classmethod
|
|
633
|
+
def set_sequence(cls, table, next_value):
|
|
634
|
+
return f"UPDATE sqlite_sequence SET seq = {next_value} WHERE name = '{table}'"
|
|
635
|
+
|
|
636
|
+
@classmethod
|
|
637
|
+
def massage_data(cls, data):
|
|
638
|
+
"""Massage data before insert/update operations."""
|
|
639
|
+
# SQLite-specific data transformations
|
|
640
|
+
massaged = {}
|
|
641
|
+
for key, value in data.items():
|
|
642
|
+
if isinstance(value, bool):
|
|
643
|
+
# Convert boolean to integer for SQLite
|
|
644
|
+
massaged[key] = 1 if value else 0
|
|
645
|
+
else:
|
|
646
|
+
massaged[key] = value
|
|
647
|
+
return massaged
|
|
648
|
+
|
|
649
|
+
@classmethod
|
|
650
|
+
def alter_trigger(cls, table, state="ENABLE", name="USER"):
|
|
651
|
+
return f"-- SQLite trigger management for {table}"
|
|
652
|
+
|
|
653
|
+
@classmethod
|
|
654
|
+
def missing(cls, tx, table, list_values, column="SYS_ID", where=None):
|
|
655
|
+
"""Generate query to find missing values from a list."""
|
|
656
|
+
# SQLite version using WITH clause
|
|
657
|
+
value_list = ", ".join([f"({i}, ?)" for i in range(len(list_values))])
|
|
658
|
+
|
|
659
|
+
sql = f"""
|
|
660
|
+
WITH input_values(pos, val) AS (
|
|
661
|
+
VALUES {value_list}
|
|
662
|
+
)
|
|
663
|
+
SELECT val FROM input_values
|
|
664
|
+
WHERE val NOT IN (
|
|
665
|
+
SELECT {quote(column)} FROM {quote(table)}
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
vals = list_values
|
|
669
|
+
|
|
670
|
+
if where:
|
|
671
|
+
where_sql, where_vals = cls._build_where(where)
|
|
672
|
+
sql += f" WHERE {where_sql}"
|
|
673
|
+
vals.extend(where_vals)
|
|
674
|
+
|
|
675
|
+
sql += ") ORDER BY pos"
|
|
676
|
+
|
|
677
|
+
return sql, vals
|