velocity-python 0.0.129__py3-none-any.whl → 0.0.132__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/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +142 -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/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +69 -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 +64 -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 +569 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +40 -0
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +4 -3
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +52 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +530 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +64 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +625 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/METADATA +1 -1
- {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/RECORD +35 -16
- velocity/db/servers/mysql.py +0 -640
- 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.129.dist-info → velocity_python-0.0.132.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
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
|
+
def quote(data):
|
|
22
|
+
"""Quote SQLite identifiers."""
|
|
23
|
+
if isinstance(data, list):
|
|
24
|
+
return [quote(item) for item in data]
|
|
25
|
+
else:
|
|
26
|
+
parts = data.split(".")
|
|
27
|
+
new = []
|
|
28
|
+
for part in parts:
|
|
29
|
+
if '"' in part:
|
|
30
|
+
new.append(part)
|
|
31
|
+
elif part.upper() in reserved_words:
|
|
32
|
+
new.append('"' + part + '"')
|
|
33
|
+
elif re.findall("[/]", part):
|
|
34
|
+
new.append('"' + part + '"')
|
|
35
|
+
else:
|
|
36
|
+
new.append(part)
|
|
37
|
+
return ".".join(new)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SQL(BaseSQLDialect):
|
|
41
|
+
server = "SQLite3"
|
|
42
|
+
type_column_identifier = "type"
|
|
43
|
+
is_nullable = "notnull"
|
|
44
|
+
|
|
45
|
+
default_schema = ""
|
|
46
|
+
|
|
47
|
+
# SQLite error codes (numeric)
|
|
48
|
+
ApplicationErrorCodes = []
|
|
49
|
+
DatabaseMissingErrorCodes = [] # SQLite creates databases on demand
|
|
50
|
+
TableMissingErrorCodes = [] # Detected by error message
|
|
51
|
+
ColumnMissingErrorCodes = [] # Detected by error message
|
|
52
|
+
ForeignKeyMissingErrorCodes = []
|
|
53
|
+
ConnectionErrorCodes = []
|
|
54
|
+
DuplicateKeyErrorCodes = [] # Detected by error message
|
|
55
|
+
RetryTransactionCodes = [] # SQLITE_BUSY
|
|
56
|
+
TruncationErrorCodes = []
|
|
57
|
+
LockTimeoutErrorCodes = [] # SQLITE_BUSY
|
|
58
|
+
DatabaseObjectExistsErrorCodes = []
|
|
59
|
+
DataIntegrityErrorCodes = []
|
|
60
|
+
|
|
61
|
+
types = TYPES
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_error(cls, e):
|
|
65
|
+
"""Extract error information from SQLite exception."""
|
|
66
|
+
# SQLite exceptions don't have error codes like other databases
|
|
67
|
+
return None, str(e)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def select(
|
|
71
|
+
cls,
|
|
72
|
+
tx,
|
|
73
|
+
columns=None,
|
|
74
|
+
table=None,
|
|
75
|
+
where=None,
|
|
76
|
+
orderby=None,
|
|
77
|
+
groupby=None,
|
|
78
|
+
having=None,
|
|
79
|
+
start=None,
|
|
80
|
+
qty=None,
|
|
81
|
+
lock=None,
|
|
82
|
+
skip_locked=None,
|
|
83
|
+
):
|
|
84
|
+
"""Generate a SQLite SELECT statement."""
|
|
85
|
+
if not table:
|
|
86
|
+
raise ValueError("Table name is required")
|
|
87
|
+
|
|
88
|
+
sql_parts = []
|
|
89
|
+
vals = []
|
|
90
|
+
|
|
91
|
+
# SELECT clause
|
|
92
|
+
if columns is None:
|
|
93
|
+
columns = ["*"]
|
|
94
|
+
elif isinstance(columns, str):
|
|
95
|
+
columns = [columns]
|
|
96
|
+
|
|
97
|
+
sql_parts.append("SELECT")
|
|
98
|
+
sql_parts.append(", ".join(columns))
|
|
99
|
+
|
|
100
|
+
# FROM clause
|
|
101
|
+
sql_parts.append("FROM")
|
|
102
|
+
sql_parts.append(quote(table))
|
|
103
|
+
|
|
104
|
+
# WHERE clause
|
|
105
|
+
if where:
|
|
106
|
+
where_sql, where_vals = cls._build_where(where)
|
|
107
|
+
sql_parts.append("WHERE")
|
|
108
|
+
sql_parts.append(where_sql)
|
|
109
|
+
vals.extend(where_vals)
|
|
110
|
+
|
|
111
|
+
# GROUP BY clause
|
|
112
|
+
if groupby:
|
|
113
|
+
if isinstance(groupby, str):
|
|
114
|
+
groupby = [groupby]
|
|
115
|
+
sql_parts.append("GROUP BY")
|
|
116
|
+
sql_parts.append(", ".join(quote(col) for col in groupby))
|
|
117
|
+
|
|
118
|
+
# HAVING clause
|
|
119
|
+
if having:
|
|
120
|
+
having_sql, having_vals = cls._build_where(having)
|
|
121
|
+
sql_parts.append("HAVING")
|
|
122
|
+
sql_parts.append(having_sql)
|
|
123
|
+
vals.extend(having_vals)
|
|
124
|
+
|
|
125
|
+
# ORDER BY clause
|
|
126
|
+
if orderby:
|
|
127
|
+
if isinstance(orderby, str):
|
|
128
|
+
orderby = [orderby]
|
|
129
|
+
elif isinstance(orderby, dict):
|
|
130
|
+
orderby_list = []
|
|
131
|
+
for col, direction in orderby.items():
|
|
132
|
+
orderby_list.append(f"{quote(col)} {direction.upper()}")
|
|
133
|
+
orderby = orderby_list
|
|
134
|
+
sql_parts.append("ORDER BY")
|
|
135
|
+
sql_parts.append(", ".join(orderby))
|
|
136
|
+
|
|
137
|
+
# LIMIT and OFFSET (SQLite syntax)
|
|
138
|
+
if qty is not None:
|
|
139
|
+
sql_parts.append(f"LIMIT {qty}")
|
|
140
|
+
if start is not None:
|
|
141
|
+
sql_parts.append(f"OFFSET {start}")
|
|
142
|
+
|
|
143
|
+
# Note: SQLite doesn't support row-level locking like FOR UPDATE
|
|
144
|
+
if lock:
|
|
145
|
+
pass # Ignored for SQLite
|
|
146
|
+
|
|
147
|
+
return " ".join(sql_parts), vals
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def _build_where(cls, where):
|
|
151
|
+
"""Build WHERE clause for SQLite."""
|
|
152
|
+
if isinstance(where, str):
|
|
153
|
+
return where, []
|
|
154
|
+
|
|
155
|
+
if isinstance(where, dict):
|
|
156
|
+
where = list(where.items())
|
|
157
|
+
|
|
158
|
+
if not isinstance(where, (list, tuple)):
|
|
159
|
+
raise ValueError("WHERE clause must be string, dict, or list")
|
|
160
|
+
|
|
161
|
+
conditions = []
|
|
162
|
+
vals = []
|
|
163
|
+
|
|
164
|
+
for key, val in where:
|
|
165
|
+
if val is None:
|
|
166
|
+
if "!" in key:
|
|
167
|
+
key = key.replace("!", "")
|
|
168
|
+
conditions.append(f"{quote(key)} IS NOT NULL")
|
|
169
|
+
else:
|
|
170
|
+
conditions.append(f"{quote(key)} IS NULL")
|
|
171
|
+
elif isinstance(val, (list, tuple)):
|
|
172
|
+
if "!" in key:
|
|
173
|
+
key = key.replace("!", "")
|
|
174
|
+
conditions.append(f"{quote(key)} NOT IN ({', '.join(['?'] * len(val))})")
|
|
175
|
+
else:
|
|
176
|
+
conditions.append(f"{quote(key)} IN ({', '.join(['?'] * len(val))})")
|
|
177
|
+
vals.extend(val)
|
|
178
|
+
else:
|
|
179
|
+
# Handle operators
|
|
180
|
+
op = "="
|
|
181
|
+
if "<>" in key:
|
|
182
|
+
key = key.replace("<>", "")
|
|
183
|
+
op = "<>"
|
|
184
|
+
elif "!=" in key:
|
|
185
|
+
key = key.replace("!=", "")
|
|
186
|
+
op = "<>"
|
|
187
|
+
elif "%" in key:
|
|
188
|
+
key = key.replace("%", "")
|
|
189
|
+
op = "LIKE"
|
|
190
|
+
elif "!" in key:
|
|
191
|
+
key = key.replace("!", "")
|
|
192
|
+
op = "<>"
|
|
193
|
+
|
|
194
|
+
conditions.append(f"{quote(key)} {op} ?")
|
|
195
|
+
vals.append(val)
|
|
196
|
+
|
|
197
|
+
return " AND ".join(conditions), vals
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def insert(cls, table, data):
|
|
201
|
+
"""Generate an INSERT statement for SQLite."""
|
|
202
|
+
if not data:
|
|
203
|
+
raise ValueError("Data cannot be empty")
|
|
204
|
+
|
|
205
|
+
columns = list(data.keys())
|
|
206
|
+
values = list(data.values())
|
|
207
|
+
|
|
208
|
+
sql_parts = [
|
|
209
|
+
"INSERT INTO",
|
|
210
|
+
quote(table),
|
|
211
|
+
f"({', '.join(quote(col) for col in columns)})",
|
|
212
|
+
"VALUES",
|
|
213
|
+
f"({', '.join(['?'] * len(values))})" # SQLite uses ? placeholders
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
return " ".join(sql_parts), values
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def update(cls, tx, table, data, where=None, pk=None, excluded=False):
|
|
220
|
+
"""Generate an UPDATE statement for SQLite."""
|
|
221
|
+
if not data:
|
|
222
|
+
raise ValueError("Data cannot be empty")
|
|
223
|
+
|
|
224
|
+
if not where and not pk:
|
|
225
|
+
raise ValueError("Either WHERE clause or primary key must be provided")
|
|
226
|
+
|
|
227
|
+
# Build SET clause
|
|
228
|
+
set_clauses = []
|
|
229
|
+
vals = []
|
|
230
|
+
|
|
231
|
+
for col, val in data.items():
|
|
232
|
+
set_clauses.append(f"{quote(col)} = ?")
|
|
233
|
+
vals.append(val)
|
|
234
|
+
|
|
235
|
+
# Build WHERE clause
|
|
236
|
+
if pk:
|
|
237
|
+
if where:
|
|
238
|
+
# Merge pk into where
|
|
239
|
+
if isinstance(where, dict):
|
|
240
|
+
where.update(pk)
|
|
241
|
+
else:
|
|
242
|
+
# Convert to dict for merging
|
|
243
|
+
where_dict = dict(where) if isinstance(where, (list, tuple)) else {}
|
|
244
|
+
where_dict.update(pk)
|
|
245
|
+
where = where_dict
|
|
246
|
+
else:
|
|
247
|
+
where = pk
|
|
248
|
+
|
|
249
|
+
where_sql, where_vals = cls._build_where(where) if where else ("", [])
|
|
250
|
+
|
|
251
|
+
sql_parts = [
|
|
252
|
+
"UPDATE",
|
|
253
|
+
quote(table),
|
|
254
|
+
"SET",
|
|
255
|
+
", ".join(set_clauses)
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
if where_sql:
|
|
259
|
+
sql_parts.extend(["WHERE", where_sql])
|
|
260
|
+
vals.extend(where_vals)
|
|
261
|
+
|
|
262
|
+
return " ".join(sql_parts), vals
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def delete(cls, tx, table, where):
|
|
266
|
+
"""Generate a DELETE statement for SQLite."""
|
|
267
|
+
if not where:
|
|
268
|
+
raise ValueError("WHERE clause is required for DELETE")
|
|
269
|
+
|
|
270
|
+
where_sql, where_vals = cls._build_where(where)
|
|
271
|
+
|
|
272
|
+
sql_parts = [
|
|
273
|
+
"DELETE FROM",
|
|
274
|
+
quote(table),
|
|
275
|
+
"WHERE",
|
|
276
|
+
where_sql
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
return " ".join(sql_parts), where_vals
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
|
|
283
|
+
"""Generate an INSERT OR REPLACE/INSERT OR IGNORE statement for SQLite."""
|
|
284
|
+
if on_conflict_do_nothing:
|
|
285
|
+
# SQLite: INSERT OR IGNORE
|
|
286
|
+
insert_sql, insert_vals = cls.insert(table, data)
|
|
287
|
+
insert_sql = insert_sql.replace("INSERT INTO", "INSERT OR IGNORE INTO")
|
|
288
|
+
return insert_sql, insert_vals
|
|
289
|
+
elif on_conflict_update:
|
|
290
|
+
# SQLite: INSERT OR REPLACE (simple replacement)
|
|
291
|
+
insert_sql, insert_vals = cls.insert(table, data)
|
|
292
|
+
insert_sql = insert_sql.replace("INSERT INTO", "INSERT OR REPLACE INTO")
|
|
293
|
+
return insert_sql, insert_vals
|
|
294
|
+
else:
|
|
295
|
+
return cls.insert(table, data)
|
|
296
|
+
|
|
297
|
+
# Metadata queries
|
|
298
|
+
@classmethod
|
|
299
|
+
def version(cls):
|
|
300
|
+
return "SELECT sqlite_version()"
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def timestamp(cls):
|
|
304
|
+
return "SELECT datetime('now')"
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def user(cls):
|
|
308
|
+
return "SELECT 'sqlite_user'" # SQLite doesn't have users
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def databases(cls):
|
|
312
|
+
return "PRAGMA database_list"
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def schemas(cls):
|
|
316
|
+
return "PRAGMA database_list"
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def current_schema(cls):
|
|
320
|
+
return "SELECT 'main'" # SQLite default schema
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def current_database(cls):
|
|
324
|
+
return "SELECT 'main'"
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def tables(cls, system=False):
|
|
328
|
+
if system:
|
|
329
|
+
return "SELECT name FROM sqlite_master WHERE type='table'"
|
|
330
|
+
else:
|
|
331
|
+
return "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def views(cls, system=False):
|
|
335
|
+
return "SELECT name FROM sqlite_master WHERE type='view'"
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def create_database(cls, name):
|
|
339
|
+
return f"-- SQLite databases are files: {name}"
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def drop_database(cls, name):
|
|
343
|
+
return f"-- SQLite databases are files: {name}"
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def create_table(cls, name, columns=None, drop=False):
|
|
347
|
+
if drop:
|
|
348
|
+
return f"DROP TABLE IF EXISTS {quote(name)}"
|
|
349
|
+
|
|
350
|
+
# Basic CREATE TABLE
|
|
351
|
+
return f"CREATE TABLE {quote(name)} (id INTEGER PRIMARY KEY AUTOINCREMENT)"
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def drop_table(cls, name):
|
|
355
|
+
return f"DROP TABLE {quote(name)}"
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def truncate(cls, table):
|
|
359
|
+
return f"DELETE FROM {quote(table)}" # SQLite doesn't have TRUNCATE
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def columns(cls, name):
|
|
363
|
+
return f"PRAGMA table_info({quote(name)})"
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def column_info(cls, table, name):
|
|
367
|
+
return f"PRAGMA table_info({quote(table)})"
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def drop_column(cls, table, name, cascade=True):
|
|
371
|
+
# SQLite doesn't support DROP COLUMN directly
|
|
372
|
+
return f"-- SQLite doesn't support DROP COLUMN for {table}.{name}"
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def alter_add(cls, table, columns, null_allowed=True):
|
|
376
|
+
alter_parts = []
|
|
377
|
+
for col, col_type in columns.items():
|
|
378
|
+
null_clause = "" if null_allowed else " NOT NULL"
|
|
379
|
+
alter_parts.append(f"ALTER TABLE {quote(table)} ADD COLUMN {quote(col)} {col_type}{null_clause}")
|
|
380
|
+
|
|
381
|
+
return "; ".join(alter_parts)
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def alter_drop(cls, table, columns):
|
|
385
|
+
return f"-- SQLite doesn't support DROP COLUMN for {table}"
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
def alter_column_by_type(cls, table, column, value, nullable=True):
|
|
389
|
+
return f"-- SQLite doesn't support ALTER COLUMN for {table}.{column}"
|
|
390
|
+
|
|
391
|
+
@classmethod
|
|
392
|
+
def alter_column_by_sql(cls, table, column, value):
|
|
393
|
+
return f"-- SQLite doesn't support ALTER COLUMN for {table}.{column}"
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def rename_column(cls, table, orig, new):
|
|
397
|
+
return f"ALTER TABLE {quote(table)} RENAME COLUMN {quote(orig)} TO {quote(new)}"
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def rename_table(cls, table, new):
|
|
401
|
+
return f"ALTER TABLE {quote(table)} RENAME TO {quote(new)}"
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
def primary_keys(cls, table):
|
|
405
|
+
return f"PRAGMA table_info({quote(table)})"
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def foreign_key_info(cls, table=None, column=None, schema=None):
|
|
409
|
+
if table:
|
|
410
|
+
return f"PRAGMA foreign_key_list({quote(table)})"
|
|
411
|
+
else:
|
|
412
|
+
return "-- SQLite foreign key info requires table name"
|
|
413
|
+
|
|
414
|
+
@classmethod
|
|
415
|
+
def create_foreign_key(cls, table, columns, key_to_table, key_to_columns, name=None, schema=None):
|
|
416
|
+
# SQLite foreign keys must be defined at table creation time
|
|
417
|
+
return f"-- SQLite foreign keys must be defined at table creation"
|
|
418
|
+
|
|
419
|
+
@classmethod
|
|
420
|
+
def drop_foreign_key(cls, table, columns, key_to_table=None, key_to_columns=None, name=None, schema=None):
|
|
421
|
+
return f"-- SQLite foreign keys must be dropped by recreating table"
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def create_index(cls, tx, table=None, columns=None, unique=False, direction=None, where=None, name=None, schema=None, trigram=None, lower=None):
|
|
425
|
+
if name is None:
|
|
426
|
+
name = f"idx_{table}_{'_'.join(columns)}"
|
|
427
|
+
|
|
428
|
+
index_type = "UNIQUE INDEX" if unique else "INDEX"
|
|
429
|
+
col_list = ", ".join(quote(col) for col in columns)
|
|
430
|
+
|
|
431
|
+
sql = f"CREATE {index_type} {quote(name)} ON {quote(table)} ({col_list})"
|
|
432
|
+
|
|
433
|
+
if where:
|
|
434
|
+
sql += f" WHERE {where}"
|
|
435
|
+
|
|
436
|
+
return sql
|
|
437
|
+
|
|
438
|
+
@classmethod
|
|
439
|
+
def drop_index(cls, table=None, columns=None, name=None, schema=None, trigram=None):
|
|
440
|
+
if name is None:
|
|
441
|
+
name = f"idx_{table}_{'_'.join(columns)}"
|
|
442
|
+
|
|
443
|
+
return f"DROP INDEX {quote(name)}"
|
|
444
|
+
|
|
445
|
+
@classmethod
|
|
446
|
+
def indexes(cls, table):
|
|
447
|
+
return f"PRAGMA index_list({quote(table)})"
|
|
448
|
+
|
|
449
|
+
@classmethod
|
|
450
|
+
def create_savepoint(cls, sp):
|
|
451
|
+
return f"SAVEPOINT {sp}"
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
def release_savepoint(cls, sp):
|
|
455
|
+
return f"RELEASE SAVEPOINT {sp}"
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def rollback_savepoint(cls, sp):
|
|
459
|
+
return f"ROLLBACK TO SAVEPOINT {sp}"
|
|
460
|
+
|
|
461
|
+
@classmethod
|
|
462
|
+
def create_view(cls, name, query, temp=False, silent=True):
|
|
463
|
+
temp_clause = "TEMPORARY " if temp else ""
|
|
464
|
+
return f"CREATE {temp_clause}VIEW {quote(name)} AS {query}"
|
|
465
|
+
|
|
466
|
+
@classmethod
|
|
467
|
+
def drop_view(cls, name, silent=True):
|
|
468
|
+
if silent:
|
|
469
|
+
return f"DROP VIEW IF EXISTS {quote(name)}"
|
|
470
|
+
else:
|
|
471
|
+
return f"DROP VIEW {quote(name)}"
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
def last_id(cls, table):
|
|
475
|
+
return "SELECT last_insert_rowid()"
|
|
476
|
+
|
|
477
|
+
@classmethod
|
|
478
|
+
def current_id(cls, table):
|
|
479
|
+
return f"SELECT seq FROM sqlite_sequence WHERE name = '{table}'"
|
|
480
|
+
|
|
481
|
+
@classmethod
|
|
482
|
+
def set_id(cls, table, start):
|
|
483
|
+
return f"UPDATE sqlite_sequence SET seq = {start} WHERE name = '{table}'"
|
|
484
|
+
|
|
485
|
+
@classmethod
|
|
486
|
+
def set_sequence(cls, table, next_value):
|
|
487
|
+
return f"UPDATE sqlite_sequence SET seq = {next_value} WHERE name = '{table}'"
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def massage_data(cls, data):
|
|
491
|
+
"""Massage data before insert/update operations."""
|
|
492
|
+
# SQLite-specific data transformations
|
|
493
|
+
massaged = {}
|
|
494
|
+
for key, value in data.items():
|
|
495
|
+
if isinstance(value, bool):
|
|
496
|
+
# Convert boolean to integer for SQLite
|
|
497
|
+
massaged[key] = 1 if value else 0
|
|
498
|
+
else:
|
|
499
|
+
massaged[key] = value
|
|
500
|
+
return massaged
|
|
501
|
+
|
|
502
|
+
@classmethod
|
|
503
|
+
def alter_trigger(cls, table, state="ENABLE", name="USER"):
|
|
504
|
+
return f"-- SQLite trigger management for {table}"
|
|
505
|
+
|
|
506
|
+
@classmethod
|
|
507
|
+
def missing(cls, tx, table, list_values, column="SYS_ID", where=None):
|
|
508
|
+
"""Generate query to find missing values from a list."""
|
|
509
|
+
# SQLite version using WITH clause
|
|
510
|
+
value_list = ", ".join([f"({i}, ?)" for i in range(len(list_values))])
|
|
511
|
+
|
|
512
|
+
sql = f"""
|
|
513
|
+
WITH input_values(pos, val) AS (
|
|
514
|
+
VALUES {value_list}
|
|
515
|
+
)
|
|
516
|
+
SELECT val FROM input_values
|
|
517
|
+
WHERE val NOT IN (
|
|
518
|
+
SELECT {quote(column)} FROM {quote(table)}
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
vals = list_values
|
|
522
|
+
|
|
523
|
+
if where:
|
|
524
|
+
where_sql, where_vals = cls._build_where(where)
|
|
525
|
+
sql += f" WHERE {where_sql}"
|
|
526
|
+
vals.extend(where_vals)
|
|
527
|
+
|
|
528
|
+
sql += ") ORDER BY pos"
|
|
529
|
+
|
|
530
|
+
return sql, vals
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import decimal
|
|
2
|
+
import datetime
|
|
3
|
+
from ..base.types import BaseTypes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TYPES(BaseTypes):
|
|
7
|
+
"""
|
|
8
|
+
SQLite-specific type mapping implementation.
|
|
9
|
+
Note: SQLite has dynamic typing, but we still define these for consistency.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
TEXT = "TEXT"
|
|
13
|
+
INTEGER = "INTEGER"
|
|
14
|
+
NUMERIC = "NUMERIC"
|
|
15
|
+
REAL = "REAL"
|
|
16
|
+
BLOB = "BLOB"
|
|
17
|
+
# SQLite doesn't have separate date/time types - they're stored as TEXT, REAL, or INTEGER
|
|
18
|
+
DATE = "TEXT"
|
|
19
|
+
TIME = "TEXT"
|
|
20
|
+
DATETIME = "TEXT"
|
|
21
|
+
TIMESTAMP = "TEXT"
|
|
22
|
+
BOOLEAN = "INTEGER" # SQLite stores booleans as integers
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_type(cls, v):
|
|
26
|
+
"""
|
|
27
|
+
Returns a suitable SQL type string for a Python value/object (SQLite).
|
|
28
|
+
"""
|
|
29
|
+
is_special, special_val = cls._handle_special_values(v)
|
|
30
|
+
if is_special:
|
|
31
|
+
return special_val
|
|
32
|
+
|
|
33
|
+
if isinstance(v, str) or v is str:
|
|
34
|
+
return cls.TEXT
|
|
35
|
+
if isinstance(v, bool) or v is bool:
|
|
36
|
+
return cls.BOOLEAN
|
|
37
|
+
if isinstance(v, int) or v is int:
|
|
38
|
+
return cls.INTEGER
|
|
39
|
+
if isinstance(v, float) or v is float:
|
|
40
|
+
return cls.REAL
|
|
41
|
+
if isinstance(v, decimal.Decimal) or v is decimal.Decimal:
|
|
42
|
+
return cls.NUMERIC
|
|
43
|
+
if isinstance(v, (datetime.datetime, datetime.date, datetime.time)) or v in (datetime.datetime, datetime.date, datetime.time):
|
|
44
|
+
return cls.TEXT # SQLite stores dates as text
|
|
45
|
+
if isinstance(v, bytes) or v is bytes:
|
|
46
|
+
return cls.BLOB
|
|
47
|
+
return cls.TEXT
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get_conv(cls, v):
|
|
51
|
+
"""
|
|
52
|
+
Returns a base SQL type for expression usage (SQLite).
|
|
53
|
+
"""
|
|
54
|
+
is_special, special_val = cls._handle_special_values(v)
|
|
55
|
+
if is_special:
|
|
56
|
+
return special_val
|
|
57
|
+
|
|
58
|
+
if isinstance(v, str) or v is str:
|
|
59
|
+
return cls.TEXT
|
|
60
|
+
if isinstance(v, bool) or v is bool:
|
|
61
|
+
return cls.BOOLEAN
|
|
62
|
+
if isinstance(v, int) or v is int:
|
|
63
|
+
return cls.INTEGER
|
|
64
|
+
if isinstance(v, float) or v is float:
|
|
65
|
+
return cls.REAL
|
|
66
|
+
if isinstance(v, decimal.Decimal) or v is decimal.Decimal:
|
|
67
|
+
return cls.NUMERIC
|
|
68
|
+
if isinstance(v, (datetime.datetime, datetime.date, datetime.time)) or v in (datetime.datetime, datetime.date, datetime.time):
|
|
69
|
+
return cls.TEXT
|
|
70
|
+
if isinstance(v, bytes) or v is bytes:
|
|
71
|
+
return cls.BLOB
|
|
72
|
+
return cls.TEXT
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def py_type(cls, v):
|
|
76
|
+
"""
|
|
77
|
+
Returns the Python type that corresponds to an SQL type string (SQLite).
|
|
78
|
+
"""
|
|
79
|
+
v = str(v).upper()
|
|
80
|
+
if v == cls.INTEGER:
|
|
81
|
+
return int
|
|
82
|
+
if v in (cls.NUMERIC, cls.REAL):
|
|
83
|
+
return float # SQLite doesn't distinguish, but float is common
|
|
84
|
+
if v == cls.TEXT:
|
|
85
|
+
return str
|
|
86
|
+
if v == cls.BOOLEAN:
|
|
87
|
+
return bool
|
|
88
|
+
if v == cls.BLOB:
|
|
89
|
+
return bytes
|
|
90
|
+
# For date/time stored as TEXT in SQLite, we'll return str
|
|
91
|
+
# The application layer needs to handle conversion
|
|
92
|
+
return str
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from ..base.initializer import BaseInitializer
|
|
3
|
+
from velocity.db.core import engine
|
|
4
|
+
from .sql import SQL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SQLServerInitializer(BaseInitializer):
|
|
8
|
+
"""SQL Server database initializer."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def initialize(config=None, **kwargs):
|
|
12
|
+
"""
|
|
13
|
+
Initialize SQL Server engine with pytds driver.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
config: Configuration dictionary
|
|
17
|
+
**kwargs: Additional configuration parameters
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Configured Engine instance
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
import pytds
|
|
24
|
+
except ImportError:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"SQL Server connector not available. Install with: pip install python-tds"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Base configuration from environment (if available)
|
|
30
|
+
base_config = {
|
|
31
|
+
"database": os.environ.get("DBDatabase"),
|
|
32
|
+
"server": os.environ.get("DBHost"), # SQL Server uses 'server' instead of 'host'
|
|
33
|
+
"port": os.environ.get("DBPort"),
|
|
34
|
+
"user": os.environ.get("DBUser"),
|
|
35
|
+
"password": os.environ.get("DBPassword"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Remove None values
|
|
39
|
+
base_config = {k: v for k, v in base_config.items() if v is not None}
|
|
40
|
+
|
|
41
|
+
# Set SQL Server-specific defaults
|
|
42
|
+
sqlserver_defaults = {
|
|
43
|
+
"server": "localhost",
|
|
44
|
+
"port": 1433,
|
|
45
|
+
"autocommit": False,
|
|
46
|
+
"timeout": 30,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Merge configurations: defaults < env < config < kwargs
|
|
50
|
+
final_config = sqlserver_defaults.copy()
|
|
51
|
+
final_config.update(base_config)
|
|
52
|
+
final_config = SQLServerInitializer._merge_config(final_config, config, **kwargs)
|
|
53
|
+
|
|
54
|
+
# Validate required configuration
|
|
55
|
+
required_keys = ["database", "server", "user"]
|
|
56
|
+
SQLServerInitializer._validate_required_config(final_config, required_keys)
|
|
57
|
+
|
|
58
|
+
return engine.Engine(pytds, final_config, SQL)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Maintain backward compatibility
|
|
62
|
+
def initialize(config=None, **kwargs):
|
|
63
|
+
"""Backward compatible initialization function."""
|
|
64
|
+
return SQLServerInitializer.initialize(config, **kwargs)
|