relationalai 0.12.13__py3-none-any.whl → 0.13.0__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.
- relationalai/__init__.py +69 -22
- relationalai/clients/__init__.py +15 -2
- relationalai/clients/client.py +4 -4
- relationalai/clients/local.py +5 -5
- relationalai/clients/resources/__init__.py +8 -0
- relationalai/clients/{azure.py → resources/azure/azure.py} +12 -12
- relationalai/clients/resources/snowflake/__init__.py +20 -0
- relationalai/clients/resources/snowflake/cli_resources.py +87 -0
- relationalai/clients/resources/snowflake/direct_access_resources.py +711 -0
- relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
- relationalai/clients/resources/snowflake/error_handlers.py +199 -0
- relationalai/clients/{export_procedure.py.jinja → resources/snowflake/export_procedure.py.jinja} +1 -1
- relationalai/clients/resources/snowflake/resources_factory.py +99 -0
- relationalai/clients/{snowflake.py → resources/snowflake/snowflake.py} +606 -1392
- relationalai/clients/{use_index_poller.py → resources/snowflake/use_index_poller.py} +43 -12
- relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
- relationalai/clients/resources/snowflake/util.py +387 -0
- relationalai/early_access/dsl/ir/executor.py +4 -4
- relationalai/early_access/dsl/snow/api.py +2 -1
- relationalai/errors.py +23 -0
- relationalai/experimental/solvers.py +7 -7
- relationalai/semantics/devtools/benchmark_lqp.py +4 -5
- relationalai/semantics/devtools/extract_lqp.py +1 -1
- relationalai/semantics/internal/internal.py +4 -4
- relationalai/semantics/internal/snowflake.py +3 -2
- relationalai/semantics/lqp/executor.py +22 -22
- relationalai/semantics/lqp/model2lqp.py +42 -4
- relationalai/semantics/lqp/passes.py +1 -1
- relationalai/semantics/lqp/rewrite/cdc.py +1 -1
- relationalai/semantics/lqp/rewrite/extract_keys.py +72 -15
- relationalai/semantics/metamodel/builtins.py +8 -6
- relationalai/semantics/metamodel/rewrite/flatten.py +9 -4
- relationalai/semantics/metamodel/util.py +6 -5
- relationalai/semantics/reasoners/graph/core.py +8 -9
- relationalai/semantics/rel/executor.py +14 -11
- relationalai/semantics/sql/compiler.py +2 -2
- relationalai/semantics/sql/executor/snowflake.py +9 -5
- relationalai/semantics/tests/test_snapshot_abstract.py +1 -1
- relationalai/tools/cli.py +26 -30
- relationalai/tools/cli_helpers.py +10 -2
- relationalai/util/otel_configuration.py +2 -1
- relationalai/util/otel_handler.py +1 -1
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/METADATA +1 -1
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/RECORD +49 -40
- relationalai_test_util/fixtures.py +2 -1
- /relationalai/clients/{cache_store.py → resources/snowflake/cache_store.py} +0 -0
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/WHEEL +0 -0
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/entry_points.txt +0 -0
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine state handlers for auto_create_engine methods using Strategy Pattern.
|
|
3
|
+
|
|
4
|
+
Each state handler encapsulates the logic for handling a specific engine state
|
|
5
|
+
(PENDING, SUSPENDED, READY, GONE, or None/missing). Handlers are separated
|
|
6
|
+
for sync and async modes since they have different behaviors.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Dict, Any, cast, Union
|
|
12
|
+
|
|
13
|
+
from ....errors import EngineNotFoundException, EngineProvisioningFailed, EngineResumeFailed, EngineSizeMismatchWarning
|
|
14
|
+
from ....tools.cli_controls import Spinner
|
|
15
|
+
from ...util import poll_with_specified_overhead
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .snowflake import Resources
|
|
19
|
+
from ...types import EngineState
|
|
20
|
+
EngineDict = Union[EngineState, Dict[str, Any]]
|
|
21
|
+
else:
|
|
22
|
+
EngineDict = Dict[str, Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class EngineContext:
|
|
27
|
+
"""Context for engine state handling."""
|
|
28
|
+
engine_name: str
|
|
29
|
+
engine_size: str | None
|
|
30
|
+
headers: Dict | None
|
|
31
|
+
requested_size: str | None # Size explicitly requested by user
|
|
32
|
+
spinner: Spinner | None = None # For async mode UI updates
|
|
33
|
+
span: Any = None # For sync mode debugging span
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EngineStateHandler(ABC):
|
|
37
|
+
"""Base class for engine state handlers using Strategy Pattern."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def handles_state(self, state: str | None) -> bool:
|
|
41
|
+
"""Check if this handler can process the given engine state."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
46
|
+
"""
|
|
47
|
+
Handle the engine state and return updated engine dict or None.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
- Updated engine dict if engine should remain
|
|
51
|
+
- None if engine should be deleted/recreated
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ============================================================================
|
|
57
|
+
# Sync Mode Handlers
|
|
58
|
+
# ============================================================================
|
|
59
|
+
|
|
60
|
+
class SyncPendingStateHandler(EngineStateHandler):
|
|
61
|
+
"""Handle PENDING state in sync mode - poll until ready."""
|
|
62
|
+
|
|
63
|
+
def handles_state(self, state: str | None) -> bool:
|
|
64
|
+
return state == "PENDING"
|
|
65
|
+
|
|
66
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
67
|
+
if not engine:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Warn if requested size doesn't match pending engine size
|
|
71
|
+
if context.requested_size is not None and engine.get("size") != context.requested_size:
|
|
72
|
+
existing_size = engine.get("size") or ""
|
|
73
|
+
EngineSizeMismatchWarning(context.engine_name, existing_size, context.requested_size)
|
|
74
|
+
|
|
75
|
+
# Poll until engine is ready
|
|
76
|
+
with Spinner(
|
|
77
|
+
"Waiting for engine to be initialized",
|
|
78
|
+
"Engine ready",
|
|
79
|
+
):
|
|
80
|
+
poll_with_specified_overhead(
|
|
81
|
+
lambda: resources.is_engine_ready(context.engine_name),
|
|
82
|
+
overhead_rate=0.1,
|
|
83
|
+
max_delay=0.5,
|
|
84
|
+
timeout=900
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Return updated engine (should be READY now)
|
|
88
|
+
updated_engine = resources.get_engine(context.engine_name)
|
|
89
|
+
return cast(EngineDict, updated_engine) if updated_engine else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SyncSuspendedStateHandler(EngineStateHandler):
|
|
93
|
+
"""Handle SUSPENDED state in sync mode - resume and poll until ready."""
|
|
94
|
+
|
|
95
|
+
def handles_state(self, state: str | None) -> bool:
|
|
96
|
+
return state == "SUSPENDED"
|
|
97
|
+
|
|
98
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
99
|
+
if not engine:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
with Spinner(
|
|
103
|
+
f"Resuming engine '{context.engine_name}'",
|
|
104
|
+
f"Engine '{context.engine_name}' resumed",
|
|
105
|
+
f"Failed to resume engine '{context.engine_name}'"
|
|
106
|
+
):
|
|
107
|
+
try:
|
|
108
|
+
resources.resume_engine_async(context.engine_name, headers=context.headers)
|
|
109
|
+
poll_with_specified_overhead(
|
|
110
|
+
lambda: resources.is_engine_ready(context.engine_name),
|
|
111
|
+
overhead_rate=0.1,
|
|
112
|
+
max_delay=0.5,
|
|
113
|
+
timeout=900
|
|
114
|
+
)
|
|
115
|
+
except Exception:
|
|
116
|
+
raise EngineResumeFailed(context.engine_name)
|
|
117
|
+
|
|
118
|
+
# Return updated engine (should be READY now)
|
|
119
|
+
updated_engine = resources.get_engine(context.engine_name)
|
|
120
|
+
return cast(EngineDict, updated_engine) if updated_engine else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SyncReadyStateHandler(EngineStateHandler):
|
|
124
|
+
"""Handle READY state in sync mode - set active and return."""
|
|
125
|
+
|
|
126
|
+
def handles_state(self, state: str | None) -> bool:
|
|
127
|
+
return state == "READY"
|
|
128
|
+
|
|
129
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
130
|
+
if not engine:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Warn if requested size doesn't match ready engine size
|
|
134
|
+
if context.requested_size is not None and engine.get("size") != context.requested_size:
|
|
135
|
+
existing_size = engine.get("size") or ""
|
|
136
|
+
EngineSizeMismatchWarning(context.engine_name, existing_size, context.requested_size)
|
|
137
|
+
|
|
138
|
+
# Cast to EngineState for _set_active_engine
|
|
139
|
+
if TYPE_CHECKING:
|
|
140
|
+
from ...types import EngineState
|
|
141
|
+
resources._set_active_engine(cast(EngineState, engine))
|
|
142
|
+
else:
|
|
143
|
+
resources._set_active_engine(engine) # type: ignore[arg-type]
|
|
144
|
+
return engine
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SyncGoneStateHandler(EngineStateHandler):
|
|
148
|
+
"""Handle GONE state in sync mode - delete and return None to trigger recreation."""
|
|
149
|
+
|
|
150
|
+
def handles_state(self, state: str | None) -> bool:
|
|
151
|
+
return state == "GONE"
|
|
152
|
+
|
|
153
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
154
|
+
if not engine:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# "Gone" is abnormal condition when metadata and SF service don't match
|
|
159
|
+
# Therefore, we have to delete the engine and create a new one
|
|
160
|
+
# It could be case that engine is already deleted, so we have to catch the exception
|
|
161
|
+
resources.delete_engine(context.engine_name, headers=context.headers)
|
|
162
|
+
# After deleting the engine, return None so that we can create a new engine
|
|
163
|
+
return None
|
|
164
|
+
except Exception as e:
|
|
165
|
+
# If engine is already deleted, we will get an exception
|
|
166
|
+
# We can ignore this exception and create a new engine
|
|
167
|
+
if isinstance(e, EngineNotFoundException):
|
|
168
|
+
return None
|
|
169
|
+
else:
|
|
170
|
+
raise EngineProvisioningFailed(context.engine_name, e) from e
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SyncMissingEngineHandler(EngineStateHandler):
|
|
174
|
+
"""Handle missing engine (None) in sync mode - create synchronously."""
|
|
175
|
+
|
|
176
|
+
def handles_state(self, state: str | None) -> bool:
|
|
177
|
+
return state is None
|
|
178
|
+
|
|
179
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
180
|
+
# This handler is called when engine doesn't exist
|
|
181
|
+
# Create engine synchronously with spinner
|
|
182
|
+
with Spinner(
|
|
183
|
+
f"Auto-creating engine {context.engine_name}",
|
|
184
|
+
f"Auto-created engine {context.engine_name}",
|
|
185
|
+
"Engine creation failed",
|
|
186
|
+
):
|
|
187
|
+
resources.create_engine(context.engine_name, size=context.engine_size, headers=context.headers)
|
|
188
|
+
|
|
189
|
+
return resources.get_engine(context.engine_name)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ============================================================================
|
|
193
|
+
# Async Mode Handlers
|
|
194
|
+
# ============================================================================
|
|
195
|
+
|
|
196
|
+
class AsyncPendingStateHandler(EngineStateHandler):
|
|
197
|
+
"""Handle PENDING state in async mode - just update spinner, don't poll."""
|
|
198
|
+
|
|
199
|
+
def handles_state(self, state: str | None) -> bool:
|
|
200
|
+
return state == "PENDING"
|
|
201
|
+
|
|
202
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
203
|
+
if not engine:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# In async mode, just update spinner - use_index will wait for engine to be ready
|
|
207
|
+
if context.spinner:
|
|
208
|
+
context.spinner.update_messages({
|
|
209
|
+
"finished_message": f"Starting engine {context.engine_name}",
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return engine
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class AsyncSuspendedStateHandler(EngineStateHandler):
|
|
216
|
+
"""Handle SUSPENDED state in async mode - resume asynchronously."""
|
|
217
|
+
|
|
218
|
+
def handles_state(self, state: str | None) -> bool:
|
|
219
|
+
return state == "SUSPENDED"
|
|
220
|
+
|
|
221
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
222
|
+
if not engine:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
if context.spinner:
|
|
226
|
+
context.spinner.update_messages({
|
|
227
|
+
"finished_message": f"Resuming engine {context.engine_name}",
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
resources.resume_engine_async(context.engine_name)
|
|
232
|
+
except Exception:
|
|
233
|
+
raise EngineResumeFailed(context.engine_name)
|
|
234
|
+
|
|
235
|
+
return engine
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class AsyncReadyStateHandler(EngineStateHandler):
|
|
239
|
+
"""Handle READY state in async mode - set active."""
|
|
240
|
+
|
|
241
|
+
def handles_state(self, state: str | None) -> bool:
|
|
242
|
+
return state == "READY"
|
|
243
|
+
|
|
244
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
245
|
+
if not engine:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
if context.spinner:
|
|
249
|
+
context.spinner.update_messages({
|
|
250
|
+
"finished_message": f"Engine {context.engine_name} initialized",
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
# Cast to EngineState for _set_active_engine
|
|
254
|
+
if TYPE_CHECKING:
|
|
255
|
+
from ...types import EngineState
|
|
256
|
+
resources._set_active_engine(cast(EngineState, engine))
|
|
257
|
+
else:
|
|
258
|
+
resources._set_active_engine(engine) # type: ignore[arg-type]
|
|
259
|
+
return engine
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class AsyncGoneStateHandler(EngineStateHandler):
|
|
263
|
+
"""Handle GONE state in async mode - delete and return None to trigger recreation."""
|
|
264
|
+
|
|
265
|
+
def handles_state(self, state: str | None) -> bool:
|
|
266
|
+
return state == "GONE"
|
|
267
|
+
|
|
268
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
269
|
+
if not engine:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
if context.spinner:
|
|
273
|
+
context.spinner.update_messages({
|
|
274
|
+
"message": f"Restarting engine {context.engine_name}",
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
# "Gone" is abnormal condition when metadata and SF service don't match
|
|
279
|
+
# Therefore, we have to delete the engine and create a new one
|
|
280
|
+
# It could be case that engine is already deleted, so we have to catch the exception
|
|
281
|
+
# Set it to None so that we can create a new engine asynchronously
|
|
282
|
+
resources.delete_engine(context.engine_name)
|
|
283
|
+
return None
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# If engine is already deleted, we will get an exception
|
|
286
|
+
# We can ignore this exception and create a new engine asynchronously
|
|
287
|
+
if isinstance(e, EngineNotFoundException):
|
|
288
|
+
return None
|
|
289
|
+
else:
|
|
290
|
+
raise EngineProvisioningFailed(context.engine_name, e) from e
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class AsyncMissingEngineHandler(EngineStateHandler):
|
|
294
|
+
"""Handle missing engine (None) in async mode - create asynchronously."""
|
|
295
|
+
|
|
296
|
+
def handles_state(self, state: str | None) -> bool:
|
|
297
|
+
return state is None
|
|
298
|
+
|
|
299
|
+
def handle(self, engine: EngineDict | None, context: EngineContext, resources: 'Resources') -> EngineDict | None:
|
|
300
|
+
# This handler is called when engine doesn't exist
|
|
301
|
+
# Create engine asynchronously
|
|
302
|
+
resources.create_engine_async(context.engine_name, size=context.engine_size)
|
|
303
|
+
|
|
304
|
+
if context.spinner:
|
|
305
|
+
context.spinner.update_messages({
|
|
306
|
+
"finished_message": f"Starting engine {context.engine_name}...",
|
|
307
|
+
})
|
|
308
|
+
return None # Engine is being created asynchronously
|
|
309
|
+
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error handlers for Snowflake Resources using Strategy Pattern.
|
|
3
|
+
|
|
4
|
+
Each error handler encapsulates the detection logic and exception creation
|
|
5
|
+
for a specific type of error. Handlers are processed in order until one matches.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import re
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from ....errors import (
|
|
13
|
+
DuoSecurityFailed,
|
|
14
|
+
EngineNotFoundException,
|
|
15
|
+
EnginePending,
|
|
16
|
+
EngineNameValidationException,
|
|
17
|
+
EngineProvisioningFailed,
|
|
18
|
+
EngineResumeFailed,
|
|
19
|
+
RAIAbortedTransactionError,
|
|
20
|
+
RAIException,
|
|
21
|
+
SnowflakeAppMissingException,
|
|
22
|
+
SnowflakeDatabaseException,
|
|
23
|
+
SnowflakeRaiAppNotStarted,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .snowflake import ExecContext
|
|
28
|
+
from .snowflake import Resources
|
|
29
|
+
|
|
30
|
+
from .util import is_database_issue, is_engine_issue, collect_error_messages
|
|
31
|
+
|
|
32
|
+
class ErrorHandler(ABC):
|
|
33
|
+
"""Base class for error handlers using Strategy Pattern."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
37
|
+
"""Check if this handler can process the error."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
42
|
+
"""Handle the error and either raise an exception or return an alternate result."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DuoSecurityErrorHandler(ErrorHandler):
|
|
47
|
+
"""Handle Duo security authentication errors."""
|
|
48
|
+
|
|
49
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
50
|
+
messages = collect_error_messages(error)
|
|
51
|
+
return any("duo security" in msg for msg in messages)
|
|
52
|
+
|
|
53
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
54
|
+
raise DuoSecurityFailed(error)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AppMissingErrorHandler(ErrorHandler):
|
|
58
|
+
"""Handle missing RAI app database errors."""
|
|
59
|
+
|
|
60
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
61
|
+
rai_app = resources.config.get("rai_app_name", "")
|
|
62
|
+
if not isinstance(rai_app, str):
|
|
63
|
+
return False
|
|
64
|
+
pattern = f"database '{rai_app}' does not exist or not authorized."
|
|
65
|
+
messages = collect_error_messages(error)
|
|
66
|
+
return any(re.search(pattern.lower(), msg) for msg in messages)
|
|
67
|
+
|
|
68
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
69
|
+
rai_app = resources.config.get("rai_app_name", "")
|
|
70
|
+
assert isinstance(rai_app, str), f"rai_app_name must be a string, not {type(rai_app)}"
|
|
71
|
+
raise SnowflakeAppMissingException(rai_app, resources.config.get("role"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DatabaseErrorsHandler(ErrorHandler):
|
|
75
|
+
"""Handle database-related errors from Snowflake/RAI."""
|
|
76
|
+
|
|
77
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
78
|
+
"""Check if error is database-related."""
|
|
79
|
+
# Use collect_error_messages to get all messages from exception chain
|
|
80
|
+
messages = collect_error_messages(error)
|
|
81
|
+
return any(is_database_issue(msg) for msg in messages)
|
|
82
|
+
|
|
83
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
84
|
+
"""Handle database errors and raise appropriate exception."""
|
|
85
|
+
raise SnowflakeDatabaseException(error)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class EngineErrorsHandler(ErrorHandler):
|
|
89
|
+
"""Handle all engine-related errors from Snowflake/RAI."""
|
|
90
|
+
|
|
91
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
92
|
+
"""Check if error is engine-related."""
|
|
93
|
+
# Use collect_error_messages to get all messages from exception chain
|
|
94
|
+
messages = collect_error_messages(error)
|
|
95
|
+
return any(is_engine_issue(msg) for msg in messages)
|
|
96
|
+
|
|
97
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
98
|
+
"""Handle engine errors and raise appropriate exception."""
|
|
99
|
+
# Use collect_error_messages to get all messages from exception chain
|
|
100
|
+
messages = collect_error_messages(error)
|
|
101
|
+
engine = resources.get_default_engine_name()
|
|
102
|
+
assert isinstance(engine, str), f"engine must be a string, not {type(engine)}"
|
|
103
|
+
|
|
104
|
+
# Check all collected messages for engine error patterns
|
|
105
|
+
for message in messages:
|
|
106
|
+
if "engine is in pending" in message or "engine is provisioning" in message:
|
|
107
|
+
raise EnginePending(engine)
|
|
108
|
+
elif "engine not found" in message or "no engines found" in message:
|
|
109
|
+
raise EngineNotFoundException(engine, str(error))
|
|
110
|
+
elif "engine was deleted" in message:
|
|
111
|
+
raise EngineNotFoundException(engine, "Engine was deleted")
|
|
112
|
+
elif "engine is suspended" in message:
|
|
113
|
+
raise EngineResumeFailed(engine)
|
|
114
|
+
elif "create/resume" in message:
|
|
115
|
+
raise EngineProvisioningFailed(engine, error)
|
|
116
|
+
|
|
117
|
+
# Generic engine error - use the original error message
|
|
118
|
+
raise RAIException(str(error))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ServiceNotStartedErrorHandler(ErrorHandler):
|
|
122
|
+
"""Handle RAI service not started errors."""
|
|
123
|
+
|
|
124
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
125
|
+
messages = collect_error_messages(error)
|
|
126
|
+
return any(re.search(r"service has not been started", msg) for msg in messages)
|
|
127
|
+
|
|
128
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
129
|
+
rai_app = resources.config.get("rai_app_name", "")
|
|
130
|
+
assert isinstance(rai_app, str), f"rai_app_name must be a string, not {type(rai_app)}"
|
|
131
|
+
raise SnowflakeRaiAppNotStarted(rai_app)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TransactionAbortedErrorHandler(ErrorHandler):
|
|
135
|
+
"""Handle transaction aborted errors with problem details."""
|
|
136
|
+
|
|
137
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
138
|
+
messages = collect_error_messages(error)
|
|
139
|
+
return any(re.search(r"state:\s*aborted", msg) for msg in messages)
|
|
140
|
+
|
|
141
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
142
|
+
# Use collect_error_messages to get all messages from exception chain
|
|
143
|
+
messages = collect_error_messages(error)
|
|
144
|
+
# Check all collected messages for transaction ID
|
|
145
|
+
for message in messages:
|
|
146
|
+
txn_id_match = re.search(r"id:\s*([0-9a-f\-]+)", message)
|
|
147
|
+
if txn_id_match:
|
|
148
|
+
problems = resources.get_transaction_problems(txn_id_match.group(1))
|
|
149
|
+
if problems:
|
|
150
|
+
# Extract problem details (handle both dict and object formats)
|
|
151
|
+
for problem in problems:
|
|
152
|
+
if isinstance(problem, dict):
|
|
153
|
+
type_field = problem.get('TYPE')
|
|
154
|
+
message_field = problem.get('MESSAGE')
|
|
155
|
+
report_field = problem.get('REPORT')
|
|
156
|
+
else:
|
|
157
|
+
type_field = problem.TYPE
|
|
158
|
+
message_field = problem.MESSAGE
|
|
159
|
+
report_field = problem.REPORT
|
|
160
|
+
raise RAIAbortedTransactionError(type_field, message_field, report_field)
|
|
161
|
+
raise RAIException(str(error))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class UseIndexRetryErrorHandler(ErrorHandler):
|
|
165
|
+
"""Handle engine/database errors by polling use_index and retrying the execution.
|
|
166
|
+
|
|
167
|
+
Intended for UseIndexResources and subclasses. Register this handler *before*
|
|
168
|
+
the standard Database/Engine error handlers.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def matches(self, error: Exception, message: str, ctx: 'ExecContext', resources: 'Resources') -> bool:
|
|
172
|
+
if ctx.skip_engine_db_error_retry:
|
|
173
|
+
return False
|
|
174
|
+
messages = collect_error_messages(error)
|
|
175
|
+
return any(msg and (is_database_issue(msg) or is_engine_issue(msg)) for msg in messages)
|
|
176
|
+
|
|
177
|
+
def handle(self, error: Exception, ctx: 'ExecContext', resources: 'Resources') -> Any | None:
|
|
178
|
+
poll_use_index = getattr(resources, "_poll_use_index", None)
|
|
179
|
+
if not callable(poll_use_index):
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
engine = resources.get_default_engine_name()
|
|
183
|
+
engine_size = resources.config.get_default_engine_size()
|
|
184
|
+
assert isinstance(engine, str), f"engine must be a string, not {type(engine)}"
|
|
185
|
+
|
|
186
|
+
model = getattr(resources, "database", "")
|
|
187
|
+
try:
|
|
188
|
+
poll_use_index(
|
|
189
|
+
app_name=resources.get_app_name(),
|
|
190
|
+
sources=resources.sources,
|
|
191
|
+
model=model,
|
|
192
|
+
engine_name=engine,
|
|
193
|
+
engine_size=engine_size,
|
|
194
|
+
)
|
|
195
|
+
except EngineNameValidationException as e:
|
|
196
|
+
raise EngineNameValidationException(engine) from e
|
|
197
|
+
|
|
198
|
+
return ctx.re_execute(resources)
|
|
199
|
+
|
relationalai/clients/{export_procedure.py.jinja → resources/snowflake/export_procedure.py.jinja}
RENAMED
|
@@ -243,7 +243,7 @@ def handle(session{{py_inputs}}, save_as_table="", passed_engine=""):
|
|
|
243
243
|
except Exception as e:
|
|
244
244
|
logging.debug(f"Error occurred: {str(e)}")
|
|
245
245
|
msg = str(e).lower()
|
|
246
|
-
if "
|
|
246
|
+
if "no columns returned" in msg or "columns of results could not be determined" in msg:
|
|
247
247
|
logging.debug("No results returned - creating empty dataframe")
|
|
248
248
|
return session.createDataFrame([], StructType([{{ py_outs }}]))
|
|
249
249
|
raise e
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory function for creating Resources instances based on configuration.
|
|
3
|
+
|
|
4
|
+
This module provides a factory function that selects the appropriate Resources
|
|
5
|
+
class based on configuration settings (platform, use_direct_access, use_graph_index).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from typing import Union
|
|
9
|
+
from snowflake.snowpark import Session
|
|
10
|
+
|
|
11
|
+
from ...config import Config
|
|
12
|
+
from ...local import LocalResources
|
|
13
|
+
from . import Resources, DirectAccessResources, UseIndexResources
|
|
14
|
+
from ....tools.constants import USE_DIRECT_ACCESS, USE_GRAPH_INDEX, Generation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_resources_instance(
|
|
18
|
+
config: Config | None = None,
|
|
19
|
+
profile: str | None = None,
|
|
20
|
+
dry_run: bool = False,
|
|
21
|
+
connection: Session | None = None,
|
|
22
|
+
reset_session: bool = False,
|
|
23
|
+
language: str = "rel",
|
|
24
|
+
generation: Generation = Generation.QB,
|
|
25
|
+
) -> Union[LocalResources, DirectAccessResources, UseIndexResources, Resources]:
|
|
26
|
+
"""
|
|
27
|
+
Factory function that creates the appropriate Resources instance based on config.
|
|
28
|
+
|
|
29
|
+
This function selects the Resources class based on:
|
|
30
|
+
1. Platform (local -> LocalResources)
|
|
31
|
+
2. use_direct_access flag (DirectAccessResources)
|
|
32
|
+
3. use_graph_index flag (UseIndexResources)
|
|
33
|
+
4. Default (base Resources)
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: Configuration object (optional, will create from profile if not provided)
|
|
37
|
+
profile: Profile name (optional, used if config is not provided)
|
|
38
|
+
dry_run: Whether to run in dry-run mode (default: False)
|
|
39
|
+
connection: Optional Snowflake session connection
|
|
40
|
+
reset_session: Whether to reset the session (default: False)
|
|
41
|
+
language: Language for the resources instance (default: "rel")
|
|
42
|
+
generation: Generation for the resources instance (default: Generation.QB)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Appropriate Resources instance based on config settings:
|
|
46
|
+
- LocalResources if platform is "local"
|
|
47
|
+
- DirectAccessResources if use_direct_access is enabled
|
|
48
|
+
- UseIndexResources if use_graph_index is enabled
|
|
49
|
+
- Resources (base) otherwise
|
|
50
|
+
"""
|
|
51
|
+
# Create config from profile if not provided
|
|
52
|
+
if config is None:
|
|
53
|
+
config = Config(profile)
|
|
54
|
+
|
|
55
|
+
platform = config.get("platform", "")
|
|
56
|
+
if platform == "local":
|
|
57
|
+
return LocalResources(
|
|
58
|
+
profile=profile,
|
|
59
|
+
config=config,
|
|
60
|
+
dry_run=dry_run,
|
|
61
|
+
generation=generation,
|
|
62
|
+
connection=connection,
|
|
63
|
+
reset_session=reset_session,
|
|
64
|
+
language=language,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if config.get("use_direct_access", USE_DIRECT_ACCESS):
|
|
68
|
+
return DirectAccessResources(
|
|
69
|
+
profile=profile,
|
|
70
|
+
config=config,
|
|
71
|
+
dry_run=dry_run,
|
|
72
|
+
generation=generation,
|
|
73
|
+
connection=connection,
|
|
74
|
+
reset_session=reset_session,
|
|
75
|
+
language=language,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if config.get("use_graph_index", USE_GRAPH_INDEX):
|
|
79
|
+
return UseIndexResources(
|
|
80
|
+
profile=profile,
|
|
81
|
+
config=config,
|
|
82
|
+
dry_run=dry_run,
|
|
83
|
+
generation=generation,
|
|
84
|
+
connection=connection,
|
|
85
|
+
reset_session=reset_session,
|
|
86
|
+
language=language,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return Resources(
|
|
90
|
+
profile=profile,
|
|
91
|
+
config=config,
|
|
92
|
+
dry_run=dry_run,
|
|
93
|
+
generation=generation,
|
|
94
|
+
connection=connection,
|
|
95
|
+
reset_session=reset_session,
|
|
96
|
+
language=language,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|