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.

Files changed (40) hide show
  1. velocity/__init__.py +1 -1
  2. velocity/aws/handlers/mixins/__init__.py +16 -0
  3. velocity/aws/handlers/mixins/activity_tracker.py +142 -0
  4. velocity/aws/handlers/mixins/error_handler.py +192 -0
  5. velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
  6. velocity/aws/handlers/mixins/standard_mixin.py +73 -0
  7. velocity/db/servers/base/__init__.py +9 -0
  8. velocity/db/servers/base/initializer.py +69 -0
  9. velocity/db/servers/base/operators.py +98 -0
  10. velocity/db/servers/base/sql.py +503 -0
  11. velocity/db/servers/base/types.py +135 -0
  12. velocity/db/servers/mysql/__init__.py +64 -0
  13. velocity/db/servers/mysql/operators.py +54 -0
  14. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  15. velocity/db/servers/mysql/sql.py +569 -0
  16. velocity/db/servers/mysql/types.py +107 -0
  17. velocity/db/servers/postgres/__init__.py +40 -0
  18. velocity/db/servers/postgres/operators.py +34 -0
  19. velocity/db/servers/postgres/sql.py +4 -3
  20. velocity/db/servers/postgres/types.py +88 -2
  21. velocity/db/servers/sqlite/__init__.py +52 -0
  22. velocity/db/servers/sqlite/operators.py +52 -0
  23. velocity/db/servers/sqlite/reserved.py +20 -0
  24. velocity/db/servers/sqlite/sql.py +530 -0
  25. velocity/db/servers/sqlite/types.py +92 -0
  26. velocity/db/servers/sqlserver/__init__.py +64 -0
  27. velocity/db/servers/sqlserver/operators.py +47 -0
  28. velocity/db/servers/sqlserver/reserved.py +32 -0
  29. velocity/db/servers/sqlserver/sql.py +625 -0
  30. velocity/db/servers/sqlserver/types.py +114 -0
  31. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/METADATA +1 -1
  32. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/RECORD +35 -16
  33. velocity/db/servers/mysql.py +0 -640
  34. velocity/db/servers/sqlite.py +0 -968
  35. velocity/db/servers/sqlite_reserved.py +0 -208
  36. velocity/db/servers/sqlserver.py +0 -921
  37. velocity/db/servers/sqlserver_reserved.py +0 -314
  38. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/WHEEL +0 -0
  39. {velocity_python-0.0.129.dist-info → velocity_python-0.0.132.dist-info}/licenses/LICENSE +0 -0
  40. {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)