relationalai 0.12.12__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.
Files changed (49) hide show
  1. relationalai/__init__.py +69 -22
  2. relationalai/clients/__init__.py +15 -2
  3. relationalai/clients/client.py +4 -4
  4. relationalai/clients/local.py +5 -5
  5. relationalai/clients/resources/__init__.py +8 -0
  6. relationalai/clients/{azure.py → resources/azure/azure.py} +12 -12
  7. relationalai/clients/resources/snowflake/__init__.py +20 -0
  8. relationalai/clients/resources/snowflake/cli_resources.py +87 -0
  9. relationalai/clients/resources/snowflake/direct_access_resources.py +711 -0
  10. relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
  11. relationalai/clients/resources/snowflake/error_handlers.py +199 -0
  12. relationalai/clients/{export_procedure.py.jinja → resources/snowflake/export_procedure.py.jinja} +1 -1
  13. relationalai/clients/resources/snowflake/resources_factory.py +99 -0
  14. relationalai/clients/{snowflake.py → resources/snowflake/snowflake.py} +635 -1380
  15. relationalai/clients/{use_index_poller.py → resources/snowflake/use_index_poller.py} +43 -12
  16. relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
  17. relationalai/clients/resources/snowflake/util.py +387 -0
  18. relationalai/early_access/dsl/ir/executor.py +4 -4
  19. relationalai/early_access/dsl/snow/api.py +2 -1
  20. relationalai/errors.py +23 -0
  21. relationalai/experimental/solvers.py +7 -7
  22. relationalai/semantics/devtools/benchmark_lqp.py +4 -5
  23. relationalai/semantics/devtools/extract_lqp.py +1 -1
  24. relationalai/semantics/internal/internal.py +4 -4
  25. relationalai/semantics/internal/snowflake.py +3 -2
  26. relationalai/semantics/lqp/executor.py +22 -22
  27. relationalai/semantics/lqp/model2lqp.py +42 -4
  28. relationalai/semantics/lqp/passes.py +1 -1
  29. relationalai/semantics/lqp/rewrite/cdc.py +1 -1
  30. relationalai/semantics/lqp/rewrite/extract_keys.py +72 -15
  31. relationalai/semantics/metamodel/builtins.py +8 -6
  32. relationalai/semantics/metamodel/rewrite/flatten.py +9 -4
  33. relationalai/semantics/metamodel/util.py +6 -5
  34. relationalai/semantics/reasoners/graph/core.py +8 -9
  35. relationalai/semantics/rel/executor.py +14 -11
  36. relationalai/semantics/sql/compiler.py +2 -2
  37. relationalai/semantics/sql/executor/snowflake.py +9 -5
  38. relationalai/semantics/tests/test_snapshot_abstract.py +1 -1
  39. relationalai/tools/cli.py +26 -30
  40. relationalai/tools/cli_helpers.py +10 -2
  41. relationalai/util/otel_configuration.py +2 -1
  42. relationalai/util/otel_handler.py +1 -1
  43. {relationalai-0.12.12.dist-info → relationalai-0.13.0.dist-info}/METADATA +1 -1
  44. {relationalai-0.12.12.dist-info → relationalai-0.13.0.dist-info}/RECORD +49 -40
  45. relationalai_test_util/fixtures.py +2 -1
  46. /relationalai/clients/{cache_store.py → resources/snowflake/cache_store.py} +0 -0
  47. {relationalai-0.12.12.dist-info → relationalai-0.13.0.dist-info}/WHEEL +0 -0
  48. {relationalai-0.12.12.dist-info → relationalai-0.13.0.dist-info}/entry_points.txt +0 -0
  49. {relationalai-0.12.12.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
+
@@ -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 "No columns returned".lower() in msg or "Columns of results could not be determined".lower() in msg:
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
+