velocity-python 0.0.109__tar.gz → 0.0.112__tar.gz
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_python-0.0.109 → velocity_python-0.0.112}/PKG-INFO +1 -1
- {velocity_python-0.0.109 → velocity_python-0.0.112}/pyproject.toml +1 -1
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/__init__.py +1 -1
- velocity_python-0.0.112/src/velocity/db/core/engine.py +481 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity_python.egg-info/PKG-INFO +1 -1
- velocity_python-0.0.109/src/velocity/db/core/engine.py +0 -1191
- {velocity_python-0.0.109 → velocity_python-0.0.112}/LICENSE +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/README.md +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/setup.cfg +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/exceptions.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/mysql.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/mysql_reserved.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/sqlite.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/sqlite_reserved.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/sqlserver.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_db.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_fix.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_format.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_iconv.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_merge.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_oconv.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_original_error.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_postgres.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_response.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.109 → velocity_python-0.0.112}/tests/test_timer.py +0 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
import traceback
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from velocity.db import exceptions
|
|
7
|
+
from velocity.db.core.transaction import Transaction
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("velocity.db.engine")
|
|
12
|
+
logger.setLevel(logging.INFO) # Or DEBUG for more verbosity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Engine:
|
|
16
|
+
"""
|
|
17
|
+
Encapsulates driver config, connection logic, error handling, and transaction decoration.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
MAX_RETRIES = 100
|
|
21
|
+
|
|
22
|
+
def __init__(self, driver, config, sql, connect_timeout=5):
|
|
23
|
+
self.__config = config
|
|
24
|
+
self.__sql = sql
|
|
25
|
+
self.__driver = driver
|
|
26
|
+
self.__connect_timeout = connect_timeout
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return f"[{self.sql.server}] engine({self.config})"
|
|
30
|
+
|
|
31
|
+
def connect(self):
|
|
32
|
+
"""
|
|
33
|
+
Connects to the database and returns the connection object.
|
|
34
|
+
If the database is missing, tries to create it, then reconnect.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
conn = self.__connect()
|
|
38
|
+
except exceptions.DbDatabaseMissingError:
|
|
39
|
+
self.create_database()
|
|
40
|
+
conn = self.__connect()
|
|
41
|
+
if self.sql.server == "SQLite3":
|
|
42
|
+
conn.isolation_level = None
|
|
43
|
+
return conn
|
|
44
|
+
|
|
45
|
+
def __connect(self):
|
|
46
|
+
"""
|
|
47
|
+
Internal connection logic, raising suitable exceptions on error.
|
|
48
|
+
Enforces a connect timeout and handles different config types.
|
|
49
|
+
"""
|
|
50
|
+
server = self.sql.server.lower()
|
|
51
|
+
timeout_key = "timeout" if "sqlite" in server else "connect_timeout"
|
|
52
|
+
timeout_val = self.__connect_timeout
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if isinstance(self.config, dict):
|
|
56
|
+
config = self.config.copy()
|
|
57
|
+
if timeout_key not in config:
|
|
58
|
+
config[timeout_key] = timeout_val
|
|
59
|
+
return self.driver.connect(**config)
|
|
60
|
+
|
|
61
|
+
elif isinstance(self.config, str):
|
|
62
|
+
conn_str = self.config
|
|
63
|
+
if timeout_key not in conn_str:
|
|
64
|
+
conn_str += f" {timeout_key}={timeout_val}"
|
|
65
|
+
return self.driver.connect(conn_str)
|
|
66
|
+
|
|
67
|
+
elif isinstance(self.config, (tuple, list)):
|
|
68
|
+
config_args = list(self.config)
|
|
69
|
+
if config_args and isinstance(config_args[-1], dict):
|
|
70
|
+
if timeout_key not in config_args[-1]:
|
|
71
|
+
config_args[-1][timeout_key] = timeout_val
|
|
72
|
+
else:
|
|
73
|
+
config_args.append({timeout_key: timeout_val})
|
|
74
|
+
return self.driver.connect(*config_args)
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"Unhandled configuration parameter type: {type(self.config)}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise self.process_error(e)
|
|
83
|
+
|
|
84
|
+
def transaction(self, func_or_cls=None):
|
|
85
|
+
"""
|
|
86
|
+
Decorator that provides a Transaction. If `tx` is passed in, uses it; otherwise, creates a new one.
|
|
87
|
+
May also be used to decorate a class, in which case all methods are wrapped in a transaction if they accept `tx`.
|
|
88
|
+
With no arguments, returns a new Transaction directly.
|
|
89
|
+
"""
|
|
90
|
+
# print("Transaction", func_or_cls.__name__, type(func_or_cls))
|
|
91
|
+
|
|
92
|
+
if func_or_cls is None:
|
|
93
|
+
return Transaction(self)
|
|
94
|
+
|
|
95
|
+
if isinstance(func_or_cls, classmethod):
|
|
96
|
+
return classmethod(self.transaction(func_or_cls.__func__))
|
|
97
|
+
|
|
98
|
+
if inspect.isfunction(func_or_cls) or inspect.ismethod(func_or_cls):
|
|
99
|
+
names = list(inspect.signature(func_or_cls).parameters.keys())
|
|
100
|
+
# print(func_or_cls.__name__, names)
|
|
101
|
+
if "_tx" in names:
|
|
102
|
+
raise NameError(
|
|
103
|
+
f"In function {func_or_cls.__name__}, '_tx' is not allowed as a parameter."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@wraps(func_or_cls)
|
|
107
|
+
def new_function(*args, **kwds):
|
|
108
|
+
tx = None
|
|
109
|
+
names = list(inspect.signature(func_or_cls).parameters.keys())
|
|
110
|
+
|
|
111
|
+
# print("inside", func_or_cls.__name__)
|
|
112
|
+
# print(names)
|
|
113
|
+
# print(args, kwds)
|
|
114
|
+
|
|
115
|
+
if "tx" not in names:
|
|
116
|
+
# The function doesn't even declare a `tx` parameter, so run normally.
|
|
117
|
+
return func_or_cls(*args, **kwds)
|
|
118
|
+
|
|
119
|
+
if "tx" in kwds:
|
|
120
|
+
if isinstance(kwds["tx"], Transaction):
|
|
121
|
+
tx = kwds["tx"]
|
|
122
|
+
else:
|
|
123
|
+
raise TypeError(
|
|
124
|
+
f"In function {func_or_cls.__name__}, keyword argument `tx` must be a Transaction object."
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
# Might be in positional args
|
|
128
|
+
pos = names.index("tx")
|
|
129
|
+
if len(args) > pos:
|
|
130
|
+
if isinstance(args[pos], Transaction):
|
|
131
|
+
tx = args[pos]
|
|
132
|
+
|
|
133
|
+
if tx:
|
|
134
|
+
return self.exec_function(func_or_cls, tx, *args, **kwds)
|
|
135
|
+
|
|
136
|
+
with Transaction(self) as local_tx:
|
|
137
|
+
pos = names.index("tx")
|
|
138
|
+
new_args = args[:pos] + (local_tx,) + args[pos:]
|
|
139
|
+
return self.exec_function(func_or_cls, local_tx, *new_args, **kwds)
|
|
140
|
+
|
|
141
|
+
return new_function
|
|
142
|
+
|
|
143
|
+
if inspect.isclass(func_or_cls):
|
|
144
|
+
|
|
145
|
+
NewCls = type(func_or_cls.__name__, (func_or_cls,), {})
|
|
146
|
+
|
|
147
|
+
for attr_name in dir(func_or_cls):
|
|
148
|
+
# Optionally skip special methods
|
|
149
|
+
if attr_name.startswith("__") and attr_name.endswith("__"):
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
attr = getattr(func_or_cls, attr_name)
|
|
153
|
+
|
|
154
|
+
if callable(attr):
|
|
155
|
+
setattr(NewCls, attr_name, self.transaction(attr))
|
|
156
|
+
|
|
157
|
+
return NewCls
|
|
158
|
+
|
|
159
|
+
return Transaction(self)
|
|
160
|
+
|
|
161
|
+
def exec_function(self, function, _tx, *args, **kwds):
|
|
162
|
+
"""
|
|
163
|
+
Executes the given function inside the transaction `_tx`.
|
|
164
|
+
Retries if it raises DbRetryTransaction or DbLockTimeoutError, up to MAX_RETRIES times.
|
|
165
|
+
"""
|
|
166
|
+
depth = getattr(_tx, "_exec_function_depth", 0)
|
|
167
|
+
setattr(_tx, "_exec_function_depth", depth + 1)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
if depth > 0:
|
|
171
|
+
# Not top-level. Just call the function.
|
|
172
|
+
return function(*args, **kwds)
|
|
173
|
+
else:
|
|
174
|
+
retry_count = 0
|
|
175
|
+
lock_timeout_count = 0
|
|
176
|
+
while True:
|
|
177
|
+
try:
|
|
178
|
+
return function(*args, **kwds)
|
|
179
|
+
except exceptions.DbRetryTransaction as e:
|
|
180
|
+
retry_count += 1
|
|
181
|
+
if retry_count > self.MAX_RETRIES:
|
|
182
|
+
raise
|
|
183
|
+
_tx.rollback()
|
|
184
|
+
except exceptions.DbLockTimeoutError as e:
|
|
185
|
+
lock_timeout_count += 1
|
|
186
|
+
if lock_timeout_count > self.MAX_RETRIES:
|
|
187
|
+
raise
|
|
188
|
+
_tx.rollback()
|
|
189
|
+
continue
|
|
190
|
+
except:
|
|
191
|
+
raise
|
|
192
|
+
finally:
|
|
193
|
+
setattr(_tx, "_exec_function_depth", depth)
|
|
194
|
+
# or if depth was 0, you might delete the attribute:
|
|
195
|
+
# if depth == 0:
|
|
196
|
+
# delattr(_tx, "_exec_function_depth")
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def driver(self):
|
|
200
|
+
return self.__driver
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def config(self):
|
|
204
|
+
return self.__config
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def sql(self):
|
|
208
|
+
return self.__sql
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def version(self):
|
|
212
|
+
"""
|
|
213
|
+
Returns the DB server version.
|
|
214
|
+
"""
|
|
215
|
+
with Transaction(self) as tx:
|
|
216
|
+
sql, vals = self.sql.version()
|
|
217
|
+
return tx.execute(sql, vals).scalar()
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def timestamp(self):
|
|
221
|
+
"""
|
|
222
|
+
Returns the current timestamp from the DB server.
|
|
223
|
+
"""
|
|
224
|
+
with Transaction(self) as tx:
|
|
225
|
+
sql, vals = self.sql.timestamp()
|
|
226
|
+
return tx.execute(sql, vals).scalar()
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def user(self):
|
|
230
|
+
"""
|
|
231
|
+
Returns the current user as known by the DB server.
|
|
232
|
+
"""
|
|
233
|
+
with Transaction(self) as tx:
|
|
234
|
+
sql, vals = self.sql.user()
|
|
235
|
+
return tx.execute(sql, vals).scalar()
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def databases(self):
|
|
239
|
+
"""
|
|
240
|
+
Returns a list of available databases.
|
|
241
|
+
"""
|
|
242
|
+
with Transaction(self) as tx:
|
|
243
|
+
sql, vals = self.sql.databases()
|
|
244
|
+
result = tx.execute(sql, vals)
|
|
245
|
+
return [x[0] for x in result.as_tuple()]
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def current_database(self):
|
|
249
|
+
"""
|
|
250
|
+
Returns the name of the current database.
|
|
251
|
+
"""
|
|
252
|
+
with Transaction(self) as tx:
|
|
253
|
+
sql, vals = self.sql.current_database()
|
|
254
|
+
return tx.execute(sql, vals).scalar()
|
|
255
|
+
|
|
256
|
+
def create_database(self, name=None):
|
|
257
|
+
"""
|
|
258
|
+
Creates a database if it doesn't exist, or does nothing if it does.
|
|
259
|
+
"""
|
|
260
|
+
old = None
|
|
261
|
+
if name is None:
|
|
262
|
+
old = self.config["database"]
|
|
263
|
+
self.set_config({"database": "postgres"})
|
|
264
|
+
name = old
|
|
265
|
+
with Transaction(self) as tx:
|
|
266
|
+
sql, vals = self.sql.create_database(name)
|
|
267
|
+
tx.execute(sql, vals, single=True)
|
|
268
|
+
if old:
|
|
269
|
+
self.set_config({"database": old})
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def switch_to_database(self, database):
|
|
273
|
+
"""
|
|
274
|
+
Switch the config to use a different database name, closing any existing connection.
|
|
275
|
+
"""
|
|
276
|
+
conf = self.config
|
|
277
|
+
if "database" in conf:
|
|
278
|
+
conf["database"] = database
|
|
279
|
+
if "dbname" in conf:
|
|
280
|
+
conf["dbname"] = database
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def set_config(self, config):
|
|
284
|
+
"""
|
|
285
|
+
Updates the internal config dictionary.
|
|
286
|
+
"""
|
|
287
|
+
self.config.update(config)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def schemas(self):
|
|
291
|
+
"""
|
|
292
|
+
Returns a list of schemas in the current database.
|
|
293
|
+
"""
|
|
294
|
+
with Transaction(self) as tx:
|
|
295
|
+
sql, vals = self.sql.schemas()
|
|
296
|
+
result = tx.execute(sql, vals)
|
|
297
|
+
return [x[0] for x in result.as_tuple()]
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def current_schema(self):
|
|
301
|
+
"""
|
|
302
|
+
Returns the current schema in use.
|
|
303
|
+
"""
|
|
304
|
+
with Transaction(self) as tx:
|
|
305
|
+
sql, vals = self.sql.current_schema()
|
|
306
|
+
return tx.execute(sql, vals).scalar()
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def tables(self):
|
|
310
|
+
"""
|
|
311
|
+
Returns a list of 'schema.table' for all tables in the current DB.
|
|
312
|
+
"""
|
|
313
|
+
with Transaction(self) as tx:
|
|
314
|
+
sql, vals = self.sql.tables()
|
|
315
|
+
result = tx.execute(sql, vals)
|
|
316
|
+
return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def views(self):
|
|
320
|
+
"""
|
|
321
|
+
Returns a list of 'schema.view' for all views in the current DB.
|
|
322
|
+
"""
|
|
323
|
+
with Transaction(self) as tx:
|
|
324
|
+
sql, vals = self.sql.views()
|
|
325
|
+
result = tx.execute(sql, vals)
|
|
326
|
+
return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
|
|
327
|
+
|
|
328
|
+
def process_error(self, exception, sql=None, parameters=None):
|
|
329
|
+
"""
|
|
330
|
+
Central method to parse driver exceptions and re-raise them as our custom exceptions.
|
|
331
|
+
"""
|
|
332
|
+
logger = logging.getLogger(__name__)
|
|
333
|
+
|
|
334
|
+
# If it's already a velocity exception, just re-raise it
|
|
335
|
+
if isinstance(exception, exceptions.DbException):
|
|
336
|
+
raise exception
|
|
337
|
+
|
|
338
|
+
# Get error code and message from the SQL driver
|
|
339
|
+
try:
|
|
340
|
+
error_code, error_message = self.sql.get_error(exception)
|
|
341
|
+
except Exception:
|
|
342
|
+
error_code, error_message = None, str(exception)
|
|
343
|
+
|
|
344
|
+
msg = str(exception).strip().lower()
|
|
345
|
+
|
|
346
|
+
# Create enhanced error message with SQL query
|
|
347
|
+
enhanced_message = str(exception)
|
|
348
|
+
if sql:
|
|
349
|
+
enhanced_message += f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
|
|
350
|
+
|
|
351
|
+
logger.warning(
|
|
352
|
+
"Database error caught. Attempting to transform: code=%s message=%s",
|
|
353
|
+
error_code,
|
|
354
|
+
error_message,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Direct error code mapping
|
|
358
|
+
if error_code in self.sql.ApplicationErrorCodes:
|
|
359
|
+
raise exceptions.DbApplicationError(enhanced_message) from exception
|
|
360
|
+
if error_code in self.sql.ColumnMissingErrorCodes:
|
|
361
|
+
raise exceptions.DbColumnMissingError(enhanced_message) from exception
|
|
362
|
+
if error_code in self.sql.TableMissingErrorCodes:
|
|
363
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
364
|
+
if error_code in self.sql.DatabaseMissingErrorCodes:
|
|
365
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
366
|
+
if error_code in self.sql.ForeignKeyMissingErrorCodes:
|
|
367
|
+
raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
|
|
368
|
+
if error_code in self.sql.TruncationErrorCodes:
|
|
369
|
+
raise exceptions.DbTruncationError(enhanced_message) from exception
|
|
370
|
+
if error_code in self.sql.DataIntegrityErrorCodes:
|
|
371
|
+
raise exceptions.DbDataIntegrityError(enhanced_message) from exception
|
|
372
|
+
if error_code in self.sql.ConnectionErrorCodes:
|
|
373
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
374
|
+
if error_code in self.sql.DuplicateKeyErrorCodes:
|
|
375
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
376
|
+
if error_code in self.sql.DatabaseObjectExistsErrorCodes:
|
|
377
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
378
|
+
if error_code in self.sql.LockTimeoutErrorCodes:
|
|
379
|
+
raise exceptions.DbLockTimeoutError(enhanced_message) from exception
|
|
380
|
+
if error_code in self.sql.RetryTransactionCodes:
|
|
381
|
+
raise exceptions.DbRetryTransaction(enhanced_message) from exception
|
|
382
|
+
|
|
383
|
+
# Regex-based fallback patterns
|
|
384
|
+
if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
|
|
385
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
386
|
+
if re.findall(r"database.*does not exist", msg, re.M):
|
|
387
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
388
|
+
if re.findall(r"no such database", msg, re.M):
|
|
389
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
390
|
+
if re.findall(r"already exists", msg, re.M):
|
|
391
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
392
|
+
if re.findall(r"server closed the connection unexpectedly", msg, re.M):
|
|
393
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
394
|
+
if re.findall(r"no connection to the server", msg, re.M):
|
|
395
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
396
|
+
if re.findall(r"connection timed out", msg, re.M):
|
|
397
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
398
|
+
if re.findall(r"could not connect to server", msg, re.M):
|
|
399
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
400
|
+
if re.findall(r"cannot connect to server", msg, re.M):
|
|
401
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
402
|
+
if re.findall(r"connection already closed", msg, re.M):
|
|
403
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
404
|
+
if re.findall(r"cursor already closed", msg, re.M):
|
|
405
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
406
|
+
if "no such table:" in msg:
|
|
407
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
408
|
+
|
|
409
|
+
logger.error(
|
|
410
|
+
"Unhandled/Unknown Error in engine.process_error",
|
|
411
|
+
exc_info=True,
|
|
412
|
+
extra={
|
|
413
|
+
"error_code": error_code,
|
|
414
|
+
"error_msg": error_message,
|
|
415
|
+
"sql_stmt": sql,
|
|
416
|
+
"sql_params": parameters,
|
|
417
|
+
},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# If we can't classify it, re-raise with enhanced message
|
|
421
|
+
raise type(exception)(enhanced_message) from exception
|
|
422
|
+
|
|
423
|
+
def _format_sql_with_params(self, sql, parameters):
|
|
424
|
+
"""
|
|
425
|
+
Format SQL query with parameters merged for easy copy-paste debugging.
|
|
426
|
+
"""
|
|
427
|
+
if not sql:
|
|
428
|
+
return "No SQL provided"
|
|
429
|
+
|
|
430
|
+
if not parameters:
|
|
431
|
+
return sql
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
# Handle different parameter formats
|
|
435
|
+
if isinstance(parameters, (list, tuple)):
|
|
436
|
+
# Convert parameters to strings and handle None values
|
|
437
|
+
formatted_params = []
|
|
438
|
+
for param in parameters:
|
|
439
|
+
if param is None:
|
|
440
|
+
formatted_params.append('NULL')
|
|
441
|
+
elif isinstance(param, str):
|
|
442
|
+
# Escape single quotes and wrap in quotes
|
|
443
|
+
escaped = param.replace("'", "''")
|
|
444
|
+
formatted_params.append(f"'{escaped}'")
|
|
445
|
+
elif isinstance(param, bool):
|
|
446
|
+
formatted_params.append('TRUE' if param else 'FALSE')
|
|
447
|
+
else:
|
|
448
|
+
formatted_params.append(str(param))
|
|
449
|
+
|
|
450
|
+
# Replace %s placeholders with actual values
|
|
451
|
+
formatted_sql = sql
|
|
452
|
+
for param in formatted_params:
|
|
453
|
+
formatted_sql = formatted_sql.replace('%s', param, 1)
|
|
454
|
+
|
|
455
|
+
return formatted_sql
|
|
456
|
+
|
|
457
|
+
elif isinstance(parameters, dict):
|
|
458
|
+
# Handle named parameters
|
|
459
|
+
formatted_sql = sql
|
|
460
|
+
for key, value in parameters.items():
|
|
461
|
+
if value is None:
|
|
462
|
+
replacement = 'NULL'
|
|
463
|
+
elif isinstance(value, str):
|
|
464
|
+
escaped = value.replace("'", "''")
|
|
465
|
+
replacement = f"'{escaped}'"
|
|
466
|
+
elif isinstance(value, bool):
|
|
467
|
+
replacement = 'TRUE' if value else 'FALSE'
|
|
468
|
+
else:
|
|
469
|
+
replacement = str(value)
|
|
470
|
+
|
|
471
|
+
# Replace %(key)s or :key patterns
|
|
472
|
+
formatted_sql = formatted_sql.replace(f'%({key})s', replacement)
|
|
473
|
+
formatted_sql = formatted_sql.replace(f':{key}', replacement)
|
|
474
|
+
|
|
475
|
+
return formatted_sql
|
|
476
|
+
else:
|
|
477
|
+
return f"{sql}\n-- Parameters: {parameters}"
|
|
478
|
+
|
|
479
|
+
except Exception as e:
|
|
480
|
+
# If formatting fails, return original SQL with parameters shown separately
|
|
481
|
+
return f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
|