relationalai 1.0.0a3__py3-none-any.whl → 1.0.0a5__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/config/config.py +47 -21
- relationalai/config/connections/__init__.py +5 -2
- relationalai/config/connections/duckdb.py +2 -2
- relationalai/config/connections/local.py +31 -0
- relationalai/config/connections/snowflake.py +0 -1
- relationalai/config/external/raiconfig_converter.py +235 -0
- relationalai/config/external/raiconfig_models.py +202 -0
- relationalai/config/external/utils.py +31 -0
- relationalai/config/shims.py +1 -0
- relationalai/semantics/__init__.py +10 -8
- relationalai/semantics/backends/sql/sql_compiler.py +1 -4
- relationalai/semantics/experimental/__init__.py +0 -0
- relationalai/semantics/experimental/builder.py +295 -0
- relationalai/semantics/experimental/builtins.py +154 -0
- relationalai/semantics/frontend/base.py +67 -42
- relationalai/semantics/frontend/core.py +34 -6
- relationalai/semantics/frontend/front_compiler.py +209 -37
- relationalai/semantics/frontend/pprint.py +6 -2
- relationalai/semantics/metamodel/__init__.py +7 -0
- relationalai/semantics/metamodel/metamodel.py +2 -0
- relationalai/semantics/metamodel/metamodel_analyzer.py +58 -16
- relationalai/semantics/metamodel/pprint.py +6 -1
- relationalai/semantics/metamodel/rewriter.py +11 -7
- relationalai/semantics/metamodel/typer.py +116 -41
- relationalai/semantics/reasoners/__init__.py +11 -0
- relationalai/semantics/reasoners/graph/__init__.py +35 -0
- relationalai/semantics/reasoners/graph/core.py +9028 -0
- relationalai/semantics/std/__init__.py +30 -10
- relationalai/semantics/std/aggregates.py +641 -12
- relationalai/semantics/std/common.py +146 -13
- relationalai/semantics/std/constraints.py +71 -1
- relationalai/semantics/std/datetime.py +904 -21
- relationalai/semantics/std/decimals.py +143 -2
- relationalai/semantics/std/floats.py +57 -4
- relationalai/semantics/std/integers.py +98 -4
- relationalai/semantics/std/math.py +857 -35
- relationalai/semantics/std/numbers.py +216 -20
- relationalai/semantics/std/re.py +213 -5
- relationalai/semantics/std/strings.py +437 -44
- relationalai/shims/executor.py +60 -52
- relationalai/shims/fixtures.py +85 -0
- relationalai/shims/helpers.py +26 -2
- relationalai/shims/hoister.py +28 -9
- relationalai/shims/mm2v0.py +204 -173
- relationalai/tools/cli/cli.py +192 -10
- relationalai/tools/cli/components/progress_reader.py +1 -1
- relationalai/tools/cli/docs.py +394 -0
- relationalai/tools/debugger.py +11 -4
- relationalai/tools/qb_debugger.py +435 -0
- relationalai/tools/typer_debugger.py +1 -2
- relationalai/util/dataclasses.py +3 -5
- relationalai/util/docutils.py +1 -2
- relationalai/util/error.py +2 -5
- relationalai/util/python.py +23 -0
- relationalai/util/runtime.py +1 -2
- relationalai/util/schema.py +2 -4
- relationalai/util/structures.py +4 -2
- relationalai/util/tracing.py +8 -2
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/METADATA +8 -5
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/RECORD +118 -95
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/WHEEL +1 -1
- v0/relationalai/__init__.py +1 -1
- v0/relationalai/clients/client.py +52 -18
- v0/relationalai/clients/exec_txn_poller.py +122 -0
- v0/relationalai/clients/local.py +23 -8
- v0/relationalai/clients/resources/azure/azure.py +36 -11
- v0/relationalai/clients/resources/snowflake/__init__.py +4 -4
- v0/relationalai/clients/resources/snowflake/cli_resources.py +12 -1
- v0/relationalai/clients/resources/snowflake/direct_access_resources.py +124 -100
- v0/relationalai/clients/resources/snowflake/engine_service.py +381 -0
- v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +35 -29
- v0/relationalai/clients/resources/snowflake/error_handlers.py +43 -2
- v0/relationalai/clients/resources/snowflake/snowflake.py +277 -179
- v0/relationalai/clients/resources/snowflake/use_index_poller.py +8 -0
- v0/relationalai/clients/types.py +5 -0
- v0/relationalai/errors.py +19 -1
- v0/relationalai/semantics/lqp/algorithms.py +173 -0
- v0/relationalai/semantics/lqp/builtins.py +199 -2
- v0/relationalai/semantics/lqp/executor.py +68 -37
- v0/relationalai/semantics/lqp/ir.py +28 -2
- v0/relationalai/semantics/lqp/model2lqp.py +215 -45
- v0/relationalai/semantics/lqp/passes.py +13 -658
- v0/relationalai/semantics/lqp/rewrite/__init__.py +12 -0
- v0/relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
- v0/relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
- v0/relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
- v0/relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
- v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
- v0/relationalai/semantics/lqp/rewrite/period_math.py +77 -0
- v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
- v0/relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
- v0/relationalai/semantics/lqp/utils.py +11 -1
- v0/relationalai/semantics/lqp/validators.py +14 -1
- v0/relationalai/semantics/metamodel/builtins.py +2 -1
- v0/relationalai/semantics/metamodel/compiler.py +2 -1
- v0/relationalai/semantics/metamodel/dependency.py +12 -3
- v0/relationalai/semantics/metamodel/executor.py +11 -1
- v0/relationalai/semantics/metamodel/factory.py +2 -2
- v0/relationalai/semantics/metamodel/helpers.py +7 -0
- v0/relationalai/semantics/metamodel/ir.py +3 -2
- v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
- v0/relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
- v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
- v0/relationalai/semantics/metamodel/typer/checker.py +6 -4
- v0/relationalai/semantics/metamodel/typer/typer.py +4 -3
- v0/relationalai/semantics/metamodel/visitor.py +4 -3
- v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
- v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +336 -86
- v0/relationalai/semantics/rel/compiler.py +2 -1
- v0/relationalai/semantics/rel/executor.py +3 -2
- v0/relationalai/semantics/tests/lqp/__init__.py +0 -0
- v0/relationalai/semantics/tests/lqp/algorithms.py +345 -0
- v0/relationalai/tools/cli.py +339 -186
- v0/relationalai/tools/cli_controls.py +216 -67
- v0/relationalai/tools/cli_helpers.py +410 -6
- v0/relationalai/util/format.py +5 -2
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/entry_points.txt +0 -0
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/top_level.txt +0 -0
|
@@ -15,6 +15,7 @@ import hashlib
|
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
17
|
from ....auth.token_handler import TokenHandler
|
|
18
|
+
from v0.relationalai.clients.exec_txn_poller import ExecTxnPoller
|
|
18
19
|
import snowflake.snowpark
|
|
19
20
|
|
|
20
21
|
from ....rel_utils import sanitize_identifier, to_fqn_relation_name
|
|
@@ -38,6 +39,7 @@ from ...types import AvailableModel, EngineState, Import, ImportSource, ImportSo
|
|
|
38
39
|
from ...config import Config
|
|
39
40
|
from ...client import Client, ExportParams, ProviderBase, ResourcesBase
|
|
40
41
|
from ...util import IdentityParser, escape_for_f_string, get_pyrel_version, get_with_retries, poll_with_specified_overhead, safe_json_loads, sanitize_module_name, scrub_exception, wrap_with_request_id, normalize_datetime
|
|
42
|
+
from .engine_service import EngineServiceSQL, EngineType
|
|
41
43
|
from .util import (
|
|
42
44
|
collect_error_messages,
|
|
43
45
|
process_jinja_template,
|
|
@@ -54,7 +56,7 @@ from .util import (
|
|
|
54
56
|
)
|
|
55
57
|
from ....environments import runtime_env, HexEnvironment, SnowbookEnvironment
|
|
56
58
|
from .... import dsl, rel, metamodel as m
|
|
57
|
-
from ....errors import EngineProvisioningFailed, EngineNameValidationException, Errors, InvalidAliasError, InvalidEngineSizeError, InvalidSourceTypeWarning, RAIException, HexSessionException, SnowflakeChangeTrackingNotEnabledException, SnowflakeDatabaseException, SnowflakeImportMissingException, SnowflakeInvalidSource, SnowflakeMissingConfigValuesException, SnowflakeProxyAPIDeprecationWarning, SnowflakeProxySourceError, ModelNotFoundException, UnknownSourceWarning, RowsDroppedFromTargetTableWarning, QueryTimeoutExceededException
|
|
59
|
+
from ....errors import EngineProvisioningFailed, EngineNameValidationException, Errors, GuardRailsException, InvalidAliasError, InvalidEngineSizeError, InvalidSourceTypeWarning, RAIException, HexSessionException, SnowflakeChangeTrackingNotEnabledException, SnowflakeDatabaseException, SnowflakeImportMissingException, SnowflakeInvalidSource, SnowflakeMissingConfigValuesException, SnowflakeProxyAPIDeprecationWarning, SnowflakeProxySourceError, ModelNotFoundException, UnknownSourceWarning, RowsDroppedFromTargetTableWarning, QueryTimeoutExceededException
|
|
58
60
|
from concurrent.futures import ThreadPoolExecutor
|
|
59
61
|
from datetime import datetime, timedelta
|
|
60
62
|
from snowflake.snowpark.types import StringType, StructField, StructType
|
|
@@ -63,6 +65,7 @@ from .error_handlers import (
|
|
|
63
65
|
ErrorHandler,
|
|
64
66
|
DuoSecurityErrorHandler,
|
|
65
67
|
AppMissingErrorHandler,
|
|
68
|
+
AppFunctionMissingErrorHandler,
|
|
66
69
|
DatabaseErrorsHandler,
|
|
67
70
|
EngineErrorsHandler,
|
|
68
71
|
ServiceNotStartedErrorHandler,
|
|
@@ -89,22 +92,26 @@ from .engine_state_handlers import (
|
|
|
89
92
|
# Constants
|
|
90
93
|
#--------------------------------------------------
|
|
91
94
|
|
|
92
|
-
VALID_POOL_STATUS = ["ACTIVE", "IDLE", "SUSPENDED"]
|
|
93
95
|
# transaction list and get return different fields (duration vs timings)
|
|
94
96
|
LIST_TXN_SQL_FIELDS = ["id", "database_name", "engine_name", "state", "abort_reason", "read_only","created_by", "created_on", "finished_at", "duration"]
|
|
95
97
|
GET_TXN_SQL_FIELDS = ["id", "database", "engine", "state", "abort_reason", "read_only","created_by", "created_on", "finished_at", "timings"]
|
|
96
98
|
VALID_ENGINE_STATES = ["READY", "PENDING"]
|
|
97
|
-
|
|
98
|
-
# Cloud-specific engine sizes
|
|
99
|
-
INTERNAL_ENGINE_SIZES = ["XS", "S", "M", "L"]
|
|
100
|
-
ENGINE_SIZES_AWS = ["HIGHMEM_X64_S", "HIGHMEM_X64_M", "HIGHMEM_X64_L"]
|
|
101
|
-
ENGINE_SIZES_AZURE = ["HIGHMEM_X64_S", "HIGHMEM_X64_M", "HIGHMEM_X64_SL"]
|
|
102
99
|
# Note: ENGINE_ERRORS, ENGINE_NOT_READY_MSGS, DATABASE_ERRORS moved to util.py
|
|
103
100
|
PYREL_ROOT_DB = 'pyrel_root_db'
|
|
104
101
|
|
|
105
102
|
TERMINAL_TXN_STATES = ["COMPLETED", "ABORTED"]
|
|
106
103
|
|
|
107
104
|
TXN_ABORT_REASON_TIMEOUT = "transaction timeout"
|
|
105
|
+
GUARDRAILS_ABORT_REASON = "guard rail violation"
|
|
106
|
+
|
|
107
|
+
PRINT_TXN_PROGRESS_FLAG = "print_txn_progress"
|
|
108
|
+
|
|
109
|
+
#--------------------------------------------------
|
|
110
|
+
# Helpers
|
|
111
|
+
#--------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def should_print_txn_progress(config) -> bool:
|
|
114
|
+
return bool(config.get(PRINT_TXN_PROGRESS_FLAG, False))
|
|
108
115
|
|
|
109
116
|
#--------------------------------------------------
|
|
110
117
|
# Resources
|
|
@@ -131,6 +138,19 @@ class ExecContext:
|
|
|
131
138
|
skip_engine_db_error_retry=self.skip_engine_db_error_retry
|
|
132
139
|
)
|
|
133
140
|
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class TxnCreationResult:
|
|
144
|
+
"""Result of creating a transaction via _create_v2_txn.
|
|
145
|
+
|
|
146
|
+
This standardizes the response format between different implementations
|
|
147
|
+
(SQL stored procedure vs HTTP direct access).
|
|
148
|
+
"""
|
|
149
|
+
txn_id: str
|
|
150
|
+
state: str
|
|
151
|
+
artifact_info: Dict[str, Dict] # Populated if fast-path (state is COMPLETED/ABORTED)
|
|
152
|
+
|
|
153
|
+
|
|
134
154
|
class Resources(ResourcesBase):
|
|
135
155
|
def __init__(
|
|
136
156
|
self,
|
|
@@ -160,11 +180,17 @@ class Resources(ResourcesBase):
|
|
|
160
180
|
self._sproc_models = None
|
|
161
181
|
# Store language for backward compatibility (used by child classes for use_index polling)
|
|
162
182
|
self.language = language
|
|
183
|
+
# Engine subsystem (composition: keeps engine CRUD isolated from the core Resources class)
|
|
184
|
+
self._engines = EngineServiceSQL(self)
|
|
163
185
|
# Register error and state handlers
|
|
164
186
|
self._register_handlers()
|
|
165
187
|
# Register atexit callback to cancel pending transactions
|
|
166
188
|
atexit.register(self.cancel_pending_transactions)
|
|
167
189
|
|
|
190
|
+
@property
|
|
191
|
+
def engines(self) -> EngineServiceSQL:
|
|
192
|
+
return self._engines
|
|
193
|
+
|
|
168
194
|
#--------------------------------------------------
|
|
169
195
|
# Initialization & Properties
|
|
170
196
|
#--------------------------------------------------
|
|
@@ -193,11 +219,12 @@ class Resources(ResourcesBase):
|
|
|
193
219
|
return handlers
|
|
194
220
|
"""
|
|
195
221
|
return [
|
|
196
|
-
DuoSecurityErrorHandler(),
|
|
197
222
|
AppMissingErrorHandler(),
|
|
223
|
+
AppFunctionMissingErrorHandler(),
|
|
224
|
+
ServiceNotStartedErrorHandler(),
|
|
225
|
+
DuoSecurityErrorHandler(),
|
|
198
226
|
DatabaseErrorsHandler(),
|
|
199
227
|
EngineErrorsHandler(),
|
|
200
|
-
ServiceNotStartedErrorHandler(),
|
|
201
228
|
TransactionAbortedErrorHandler(),
|
|
202
229
|
]
|
|
203
230
|
|
|
@@ -589,7 +616,7 @@ class Resources(ResourcesBase):
|
|
|
589
616
|
|
|
590
617
|
# Validate engine size
|
|
591
618
|
if engine_size:
|
|
592
|
-
is_size_valid, sizes = self.validate_engine_size(engine_size)
|
|
619
|
+
is_size_valid, sizes = self._engines.validate_engine_size(engine_size)
|
|
593
620
|
if not is_size_valid:
|
|
594
621
|
error_msg = f"Invalid engine size '{engine_size}'. Valid sizes are: {', '.join(sizes)}"
|
|
595
622
|
if use_default_size:
|
|
@@ -679,67 +706,27 @@ class Resources(ResourcesBase):
|
|
|
679
706
|
if not handled:
|
|
680
707
|
raise EngineProvisioningFailed(engine_name, error) from error
|
|
681
708
|
|
|
682
|
-
def validate_engine_size(self, size: str) -> Tuple[bool, List[str]]:
|
|
683
|
-
if size is not None:
|
|
684
|
-
sizes = self.get_engine_sizes()
|
|
685
|
-
if size not in sizes:
|
|
686
|
-
return False, sizes
|
|
687
|
-
return True, []
|
|
688
|
-
|
|
689
709
|
def get_engine_sizes(self, cloud_provider: str|None=None):
|
|
690
|
-
|
|
691
|
-
if cloud_provider is None:
|
|
692
|
-
cloud_provider = self.get_cloud_provider()
|
|
693
|
-
if cloud_provider == 'azure':
|
|
694
|
-
sizes = ENGINE_SIZES_AZURE
|
|
695
|
-
else:
|
|
696
|
-
sizes = ENGINE_SIZES_AWS
|
|
697
|
-
if self.config.show_all_engine_sizes():
|
|
698
|
-
return INTERNAL_ENGINE_SIZES + sizes
|
|
699
|
-
else:
|
|
700
|
-
return sizes
|
|
701
|
-
|
|
702
|
-
def list_engines(self, state: str | None = None):
|
|
703
|
-
where_clause = f"WHERE STATUS = '{state.upper()}'" if state else ""
|
|
704
|
-
statement = f"SELECT NAME, ID, SIZE, STATUS, CREATED_BY, CREATED_ON, UPDATED_ON FROM {APP_NAME}.api.engines {where_clause} ORDER BY NAME ASC;"
|
|
705
|
-
results = self._exec(statement)
|
|
706
|
-
if not results:
|
|
707
|
-
return []
|
|
708
|
-
return [
|
|
709
|
-
{
|
|
710
|
-
"name": row["NAME"],
|
|
711
|
-
"id": row["ID"],
|
|
712
|
-
"size": row["SIZE"],
|
|
713
|
-
"state": row["STATUS"], # callers are expecting 'state'
|
|
714
|
-
"created_by": row["CREATED_BY"],
|
|
715
|
-
"created_on": row["CREATED_ON"],
|
|
716
|
-
"updated_on": row["UPDATED_ON"],
|
|
717
|
-
}
|
|
718
|
-
for row in results
|
|
719
|
-
]
|
|
710
|
+
return self._engines.get_engine_sizes(cloud_provider=cloud_provider)
|
|
720
711
|
|
|
721
|
-
def
|
|
722
|
-
|
|
723
|
-
|
|
712
|
+
def list_engines(
|
|
713
|
+
self,
|
|
714
|
+
state: str | None = None,
|
|
715
|
+
name: str | None = None,
|
|
716
|
+
type: str | None = None,
|
|
717
|
+
size: str | None = None,
|
|
718
|
+
created_by: str | None = None,
|
|
719
|
+
):
|
|
720
|
+
return self._engines.list_engines(
|
|
721
|
+
state=state,
|
|
722
|
+
name=name,
|
|
723
|
+
type=type,
|
|
724
|
+
size=size,
|
|
725
|
+
created_by=created_by,
|
|
724
726
|
)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if not engine:
|
|
729
|
-
return None
|
|
730
|
-
engine_state: EngineState = {
|
|
731
|
-
"name": engine["NAME"],
|
|
732
|
-
"id": engine["ID"],
|
|
733
|
-
"size": engine["SIZE"],
|
|
734
|
-
"state": engine["STATUS"], # callers are expecting 'state'
|
|
735
|
-
"created_by": engine["CREATED_BY"],
|
|
736
|
-
"created_on": engine["CREATED_ON"],
|
|
737
|
-
"updated_on": engine["UPDATED_ON"],
|
|
738
|
-
"version": engine["VERSION"],
|
|
739
|
-
"auto_suspend": engine["AUTO_SUSPEND_MINS"],
|
|
740
|
-
"suspends_at": engine["SUSPENDS_AT"]
|
|
741
|
-
}
|
|
742
|
-
return engine_state
|
|
727
|
+
|
|
728
|
+
def get_engine(self, name: str, type: str):
|
|
729
|
+
return self._engines.get_engine(name, type)
|
|
743
730
|
|
|
744
731
|
def get_default_engine_name(self) -> str:
|
|
745
732
|
if self.config.get("engine_name", None) is not None:
|
|
@@ -760,60 +747,82 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
760
747
|
def is_valid_engine_state(self, name:str):
|
|
761
748
|
return name in VALID_ENGINE_STATES
|
|
762
749
|
|
|
750
|
+
# Can be overridden by subclasses (e.g. DirectAccessResources)
|
|
763
751
|
def _create_engine(
|
|
764
752
|
self,
|
|
765
753
|
name: str,
|
|
754
|
+
type: str = EngineType.LOGIC,
|
|
766
755
|
size: str | None = None,
|
|
767
756
|
auto_suspend_mins: int | None= None,
|
|
768
757
|
is_async: bool = False,
|
|
769
758
|
headers: Dict | None = None,
|
|
759
|
+
settings: Dict[str, Any] | None = None,
|
|
770
760
|
):
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
# check in case the config default is missing
|
|
781
|
-
if auto_suspend_mins is None:
|
|
782
|
-
self._exec(f"call {APP_NAME}.api.{api}('{name}', '{size}', null, {headers});")
|
|
783
|
-
else:
|
|
784
|
-
self._exec(f"call {APP_NAME}.api.{api}('{name}', '{size}', PARSE_JSON('{{\"auto_suspend_mins\": {auto_suspend_mins}}}'), {headers});")
|
|
785
|
-
except Exception as e:
|
|
786
|
-
raise EngineProvisioningFailed(name, e) from e
|
|
761
|
+
return self._engines._create_engine(
|
|
762
|
+
name=name,
|
|
763
|
+
type=type,
|
|
764
|
+
size=size,
|
|
765
|
+
auto_suspend_mins=auto_suspend_mins,
|
|
766
|
+
is_async=is_async,
|
|
767
|
+
headers=headers,
|
|
768
|
+
settings=settings,
|
|
769
|
+
)
|
|
787
770
|
|
|
788
|
-
def create_engine(
|
|
789
|
-
self
|
|
771
|
+
def create_engine(
|
|
772
|
+
self,
|
|
773
|
+
name: str,
|
|
774
|
+
type: str | None = None,
|
|
775
|
+
size: str | None = None,
|
|
776
|
+
auto_suspend_mins: int | None = None,
|
|
777
|
+
headers: Dict | None = None,
|
|
778
|
+
settings: Dict[str, Any] | None = None,
|
|
779
|
+
):
|
|
780
|
+
if type is None:
|
|
781
|
+
type = EngineType.LOGIC
|
|
782
|
+
# Route through _create_engine so subclasses (e.g. DirectAccessResources)
|
|
783
|
+
# can override engine creation behavior.
|
|
784
|
+
return self._create_engine(
|
|
785
|
+
name=name,
|
|
786
|
+
type=type,
|
|
787
|
+
size=size,
|
|
788
|
+
auto_suspend_mins=auto_suspend_mins,
|
|
789
|
+
is_async=False,
|
|
790
|
+
headers=headers,
|
|
791
|
+
settings=settings,
|
|
792
|
+
)
|
|
790
793
|
|
|
791
|
-
def create_engine_async(
|
|
792
|
-
self
|
|
794
|
+
def create_engine_async(
|
|
795
|
+
self,
|
|
796
|
+
name: str,
|
|
797
|
+
type: str = EngineType.LOGIC,
|
|
798
|
+
size: str | None = None,
|
|
799
|
+
auto_suspend_mins: int | None = None,
|
|
800
|
+
):
|
|
801
|
+
# Route through _create_engine so subclasses (e.g. DirectAccessResources)
|
|
802
|
+
# can override async engine creation behavior.
|
|
803
|
+
return self._create_engine(
|
|
804
|
+
name=name,
|
|
805
|
+
type=type,
|
|
806
|
+
size=size,
|
|
807
|
+
auto_suspend_mins=auto_suspend_mins,
|
|
808
|
+
is_async=True,
|
|
809
|
+
)
|
|
793
810
|
|
|
794
|
-
def delete_engine(self, name:str,
|
|
795
|
-
|
|
796
|
-
self._exec(f"call {APP_NAME}.api.delete_engine('{name}', {force},{request_headers});")
|
|
811
|
+
def delete_engine(self, name: str, type: str):
|
|
812
|
+
return self._engines.delete_engine(name, type)
|
|
797
813
|
|
|
798
|
-
def suspend_engine(self, name:str):
|
|
799
|
-
self.
|
|
814
|
+
def suspend_engine(self, name: str, type: str | None = None):
|
|
815
|
+
return self._engines.suspend_engine(name, type)
|
|
800
816
|
|
|
801
|
-
def resume_engine(self, name:str, headers: Dict | None = None) -> Dict:
|
|
802
|
-
|
|
803
|
-
self._exec(f"call {APP_NAME}.api.resume_engine('{name}',{request_headers});")
|
|
804
|
-
# returning empty dict to match the expected return type
|
|
805
|
-
return {}
|
|
817
|
+
def resume_engine(self, name: str, type: str | None = None, headers: Dict | None = None) -> Dict:
|
|
818
|
+
return self._engines.resume_engine(name, type=type, headers=headers)
|
|
806
819
|
|
|
807
|
-
def resume_engine_async(self, name:str, headers: Dict | None = None) -> Dict:
|
|
808
|
-
|
|
809
|
-
headers = {}
|
|
810
|
-
self._exec(f"call {APP_NAME}.api.resume_engine_async('{name}',{headers});")
|
|
811
|
-
# returning empty dict to match the expected return type
|
|
812
|
-
return {}
|
|
820
|
+
def resume_engine_async(self, name: str, type: str | None = None, headers: Dict | None = None) -> Dict:
|
|
821
|
+
return self._engines.resume_engine_async(name, type=type, headers=headers)
|
|
813
822
|
|
|
814
823
|
def alter_engine_pool(self, size:str|None=None, mins:int|None=None, maxs:int|None=None):
|
|
815
824
|
"""Alter engine pool node limits for Snowflake."""
|
|
816
|
-
self.
|
|
825
|
+
return self._engines.alter_engine_pool(size=size, mins=mins, maxs=maxs)
|
|
817
826
|
|
|
818
827
|
#--------------------------------------------------
|
|
819
828
|
# Graphs
|
|
@@ -1411,15 +1420,18 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1411
1420
|
if txn_id in self._pending_transactions:
|
|
1412
1421
|
self._pending_transactions.remove(txn_id)
|
|
1413
1422
|
|
|
1414
|
-
if status == "ABORTED"
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
+
if status == "ABORTED":
|
|
1424
|
+
if response_row.get("ABORT_REASON", "") == TXN_ABORT_REASON_TIMEOUT:
|
|
1425
|
+
config_file_path = getattr(self.config, 'file_path', None)
|
|
1426
|
+
# todo: use the timeout returned alongside the transaction as soon as it's exposed
|
|
1427
|
+
timeout_mins = int(self.config.get("query_timeout_mins", DEFAULT_QUERY_TIMEOUT_MINS) or DEFAULT_QUERY_TIMEOUT_MINS)
|
|
1428
|
+
raise QueryTimeoutExceededException(
|
|
1429
|
+
timeout_mins=timeout_mins,
|
|
1430
|
+
query_id=txn_id,
|
|
1431
|
+
config_file_path=config_file_path,
|
|
1432
|
+
)
|
|
1433
|
+
elif response_row.get("ABORT_REASON", "") == GUARDRAILS_ABORT_REASON:
|
|
1434
|
+
raise GuardRailsException()
|
|
1423
1435
|
|
|
1424
1436
|
# @TODO: Find some way to tunnel the ABORT_REASON out. Azure doesn't have this, but it's handy
|
|
1425
1437
|
return status == "COMPLETED" or status == "ABORTED"
|
|
@@ -1654,6 +1666,72 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1654
1666
|
raise Exception("Failed to create transaction")
|
|
1655
1667
|
return response
|
|
1656
1668
|
|
|
1669
|
+
def _create_v2_txn(
|
|
1670
|
+
self,
|
|
1671
|
+
database: str,
|
|
1672
|
+
engine: str | None,
|
|
1673
|
+
raw_code: str,
|
|
1674
|
+
inputs: Dict,
|
|
1675
|
+
headers: Dict[str, str],
|
|
1676
|
+
readonly: bool,
|
|
1677
|
+
nowait_durable: bool,
|
|
1678
|
+
bypass_index: bool,
|
|
1679
|
+
language: str,
|
|
1680
|
+
query_timeout_mins: int | None,
|
|
1681
|
+
) -> TxnCreationResult:
|
|
1682
|
+
"""
|
|
1683
|
+
Create a transaction and return the result.
|
|
1684
|
+
|
|
1685
|
+
This method handles calling the RAI app stored procedure to create a transaction
|
|
1686
|
+
and parses the response into a standardized TxnCreationResult format.
|
|
1687
|
+
|
|
1688
|
+
This method can be overridden by subclasses (e.g., DirectAccessResources)
|
|
1689
|
+
to use different transport mechanisms (HTTP instead of SQL).
|
|
1690
|
+
|
|
1691
|
+
Args:
|
|
1692
|
+
database: Database/model name
|
|
1693
|
+
engine: Engine name (optional)
|
|
1694
|
+
raw_code: Code to execute (REL, LQP, or SQL)
|
|
1695
|
+
inputs: Input parameters for the query
|
|
1696
|
+
headers: HTTP headers (must be prepared by caller)
|
|
1697
|
+
readonly: Whether the transaction is read-only
|
|
1698
|
+
nowait_durable: Whether to wait for durable writes
|
|
1699
|
+
bypass_index: Whether to bypass graph index setup
|
|
1700
|
+
language: Query language ("rel" or "lqp")
|
|
1701
|
+
query_timeout_mins: Optional query timeout in minutes
|
|
1702
|
+
|
|
1703
|
+
Returns:
|
|
1704
|
+
TxnCreationResult containing txn_id, state, and artifact_info
|
|
1705
|
+
"""
|
|
1706
|
+
response = self._exec_rai_app(
|
|
1707
|
+
database=database,
|
|
1708
|
+
engine=engine,
|
|
1709
|
+
raw_code=raw_code,
|
|
1710
|
+
inputs=inputs,
|
|
1711
|
+
readonly=readonly,
|
|
1712
|
+
nowait_durable=nowait_durable,
|
|
1713
|
+
request_headers=headers,
|
|
1714
|
+
bypass_index=bypass_index,
|
|
1715
|
+
language=language,
|
|
1716
|
+
query_timeout_mins=query_timeout_mins,
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1719
|
+
rows = list(iter(response))
|
|
1720
|
+
|
|
1721
|
+
# process the first row since txn_id and state are the same for all rows
|
|
1722
|
+
first_row = rows[0]
|
|
1723
|
+
txn_id = first_row['ID']
|
|
1724
|
+
state = first_row['STATE']
|
|
1725
|
+
|
|
1726
|
+
# Build artifact_info if transaction completed immediately (fast path)
|
|
1727
|
+
artifact_info: Dict[str, Dict] = {}
|
|
1728
|
+
if state in ["COMPLETED", "ABORTED"]:
|
|
1729
|
+
for row in rows:
|
|
1730
|
+
filename = row['FILENAME']
|
|
1731
|
+
artifact_info[filename] = row
|
|
1732
|
+
|
|
1733
|
+
return TxnCreationResult(txn_id=txn_id, state=state, artifact_info=artifact_info)
|
|
1734
|
+
|
|
1657
1735
|
def _exec_async_v2(
|
|
1658
1736
|
self,
|
|
1659
1737
|
database: str,
|
|
@@ -1672,15 +1750,20 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1672
1750
|
High-level async execution method with transaction polling and artifact management.
|
|
1673
1751
|
|
|
1674
1752
|
This is the core method for executing queries asynchronously. It:
|
|
1675
|
-
1. Creates a transaction by calling
|
|
1753
|
+
1. Creates a transaction by calling _create_v2_txn
|
|
1676
1754
|
2. Handles two execution paths:
|
|
1677
1755
|
- Fast path: Transaction completes immediately (COMPLETED/ABORTED)
|
|
1678
1756
|
- Slow path: Transaction is pending, requires polling until completion
|
|
1679
1757
|
3. Manages pending transactions list
|
|
1680
1758
|
4. Downloads and returns query results/artifacts
|
|
1681
1759
|
|
|
1682
|
-
This method is called by _execute_code (base implementation) and
|
|
1683
|
-
overridden by child classes (e.g.,
|
|
1760
|
+
This method is called by _execute_code (base implementation), and calls the
|
|
1761
|
+
following methods that can be overridden by child classes (e.g.,
|
|
1762
|
+
DirectAccessResources uses HTTP instead):
|
|
1763
|
+
- _create_v2_txn
|
|
1764
|
+
- _check_exec_async_status
|
|
1765
|
+
- _list_exec_async_artifacts
|
|
1766
|
+
- _download_results
|
|
1684
1767
|
|
|
1685
1768
|
Args:
|
|
1686
1769
|
database: Database/model name
|
|
@@ -1704,57 +1787,62 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1704
1787
|
query_attrs_dict = json.loads(request_headers.get("X-Query-Attributes", "{}"))
|
|
1705
1788
|
|
|
1706
1789
|
with debugging.span("transaction", **query_attrs_dict) as txn_span:
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
request_headers=
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1790
|
+
txn_start_time = time.time()
|
|
1791
|
+
print_txn_progress = should_print_txn_progress(self.config)
|
|
1792
|
+
|
|
1793
|
+
with ExecTxnPoller(
|
|
1794
|
+
print_txn_progress=print_txn_progress,
|
|
1795
|
+
resource=self, txn_id=None, headers=request_headers,
|
|
1796
|
+
txn_start_time=txn_start_time
|
|
1797
|
+
) as poller:
|
|
1798
|
+
with debugging.span("create_v2", **query_attrs_dict) as create_span:
|
|
1799
|
+
# Prepare headers for transaction creation
|
|
1800
|
+
request_headers['user-agent'] = get_pyrel_version(self.generation)
|
|
1801
|
+
request_headers['gi_setup_skipped'] = str(gi_setup_skipped)
|
|
1802
|
+
request_headers['pyrel_program_id'] = debugging.get_program_span_id() or ""
|
|
1803
|
+
|
|
1804
|
+
# Create the transaction
|
|
1805
|
+
result = self._create_v2_txn(
|
|
1806
|
+
database=database,
|
|
1807
|
+
engine=engine,
|
|
1808
|
+
raw_code=raw_code,
|
|
1809
|
+
inputs=inputs,
|
|
1810
|
+
headers=request_headers,
|
|
1811
|
+
readonly=readonly,
|
|
1812
|
+
nowait_durable=nowait_durable,
|
|
1813
|
+
bypass_index=bypass_index,
|
|
1814
|
+
language=language,
|
|
1815
|
+
query_timeout_mins=query_timeout_mins,
|
|
1816
|
+
)
|
|
1723
1817
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1818
|
+
txn_id = result.txn_id
|
|
1819
|
+
state = result.state
|
|
1726
1820
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
state = first_row['STATE']
|
|
1731
|
-
filename = first_row['FILENAME']
|
|
1821
|
+
txn_span["txn_id"] = txn_id
|
|
1822
|
+
create_span["txn_id"] = txn_id
|
|
1823
|
+
debugging.event("transaction_created", txn_span, txn_id=txn_id)
|
|
1732
1824
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
debugging.event("transaction_created", txn_span, txn_id=txn_id)
|
|
1825
|
+
# Set the transaction ID now that we have it, to update the progress text
|
|
1826
|
+
poller.txn_id = txn_id
|
|
1736
1827
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1828
|
+
# fast path: transaction already finished
|
|
1829
|
+
if state in ["COMPLETED", "ABORTED"]:
|
|
1830
|
+
if txn_id in self._pending_transactions:
|
|
1831
|
+
self._pending_transactions.remove(txn_id)
|
|
1741
1832
|
|
|
1742
|
-
|
|
1743
|
-
for row in rows:
|
|
1744
|
-
filename = row['FILENAME']
|
|
1745
|
-
artifact_info[filename] = row
|
|
1833
|
+
artifact_info = result.artifact_info
|
|
1746
1834
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1835
|
+
# Slow path: transaction not done yet; start polling
|
|
1836
|
+
else:
|
|
1837
|
+
self._pending_transactions.append(txn_id)
|
|
1838
|
+
# Use the interactive poller for transaction status
|
|
1839
|
+
with debugging.span("wait", txn_id=txn_id):
|
|
1840
|
+
poller.poll()
|
|
1841
|
+
|
|
1842
|
+
artifact_info = self._list_exec_async_artifacts(txn_id, headers=request_headers)
|
|
1755
1843
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1844
|
+
with debugging.span("fetch"):
|
|
1845
|
+
return self._download_results(artifact_info, txn_id, state)
|
|
1758
1846
|
|
|
1759
1847
|
def get_user_based_engine_name(self):
|
|
1760
1848
|
if not self._session:
|
|
@@ -1764,11 +1852,17 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1764
1852
|
assert isinstance(user, str), f"current_user() must return a string, not {type(user)}"
|
|
1765
1853
|
return _sanitize_user_name(user)
|
|
1766
1854
|
|
|
1767
|
-
def is_engine_ready(self, engine_name: str):
|
|
1768
|
-
engine = self.get_engine(engine_name)
|
|
1855
|
+
def is_engine_ready(self, engine_name: str, type: str = EngineType.LOGIC):
|
|
1856
|
+
engine = self.get_engine(engine_name, type)
|
|
1769
1857
|
return engine and engine["state"] == "READY"
|
|
1770
1858
|
|
|
1771
|
-
def auto_create_engine(
|
|
1859
|
+
def auto_create_engine(
|
|
1860
|
+
self,
|
|
1861
|
+
name: str | None = None,
|
|
1862
|
+
type: str = EngineType.LOGIC,
|
|
1863
|
+
size: str | None = None,
|
|
1864
|
+
headers: Dict | None = None,
|
|
1865
|
+
):
|
|
1772
1866
|
"""Synchronously create/ensure an engine is ready, blocking until ready."""
|
|
1773
1867
|
with debugging.span("auto_create_engine", active=self._active_engine) as span:
|
|
1774
1868
|
active = self._get_active_engine()
|
|
@@ -1776,18 +1870,19 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1776
1870
|
return active
|
|
1777
1871
|
|
|
1778
1872
|
# Resolve and validate parameters
|
|
1779
|
-
|
|
1873
|
+
name, size = self._prepare_engine_params(name, size)
|
|
1780
1874
|
|
|
1781
1875
|
try:
|
|
1782
1876
|
# Get current engine state
|
|
1783
|
-
engine = self.get_engine(
|
|
1877
|
+
engine = self.get_engine(name, type)
|
|
1784
1878
|
if engine:
|
|
1785
1879
|
span.update(cast(dict, engine))
|
|
1786
1880
|
|
|
1787
1881
|
# Create context for state handling
|
|
1788
1882
|
context = EngineContext(
|
|
1789
|
-
|
|
1790
|
-
|
|
1883
|
+
name=name,
|
|
1884
|
+
size=size,
|
|
1885
|
+
type=type,
|
|
1791
1886
|
headers=headers,
|
|
1792
1887
|
requested_size=size,
|
|
1793
1888
|
span=span,
|
|
@@ -1797,12 +1892,14 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1797
1892
|
self._process_engine_state(engine, context, self._sync_engine_state_handlers)
|
|
1798
1893
|
|
|
1799
1894
|
except Exception as e:
|
|
1800
|
-
self._handle_engine_creation_errors(e,
|
|
1895
|
+
self._handle_engine_creation_errors(e, name)
|
|
1801
1896
|
|
|
1802
|
-
return
|
|
1897
|
+
return name
|
|
1803
1898
|
|
|
1804
|
-
def auto_create_engine_async(self, name: str | None = None):
|
|
1899
|
+
def auto_create_engine_async(self, name: str | None = None, type: str | None = None):
|
|
1805
1900
|
"""Asynchronously create/ensure an engine, returns immediately."""
|
|
1901
|
+
if type is None:
|
|
1902
|
+
type = EngineType.LOGIC
|
|
1806
1903
|
active = self._get_active_engine()
|
|
1807
1904
|
if active and (active == name or name is None):
|
|
1808
1905
|
return active
|
|
@@ -1813,16 +1910,17 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1813
1910
|
) as spinner:
|
|
1814
1911
|
with debugging.span("auto_create_engine_async", active=self._active_engine):
|
|
1815
1912
|
# Resolve and validate parameters (use_default_size=True for async)
|
|
1816
|
-
|
|
1913
|
+
name, size = self._prepare_engine_params(name, None, use_default_size=True)
|
|
1817
1914
|
|
|
1818
1915
|
try:
|
|
1819
1916
|
# Get current engine state
|
|
1820
|
-
engine = self.get_engine(
|
|
1917
|
+
engine = self.get_engine(name, type)
|
|
1821
1918
|
|
|
1822
1919
|
# Create context for state handling
|
|
1823
1920
|
context = EngineContext(
|
|
1824
|
-
|
|
1825
|
-
|
|
1921
|
+
name=name,
|
|
1922
|
+
size=size,
|
|
1923
|
+
type=type,
|
|
1826
1924
|
headers=None,
|
|
1827
1925
|
requested_size=None,
|
|
1828
1926
|
spinner=spinner,
|
|
@@ -1833,11 +1931,11 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1833
1931
|
|
|
1834
1932
|
except Exception as e:
|
|
1835
1933
|
spinner.update_messages({
|
|
1836
|
-
"finished_message": f"Failed to create engine {
|
|
1934
|
+
"finished_message": f"Failed to create engine {name}",
|
|
1837
1935
|
})
|
|
1838
|
-
self._handle_engine_creation_errors(e,
|
|
1936
|
+
self._handle_engine_creation_errors(e, name, preserve_rai_exception=True)
|
|
1839
1937
|
|
|
1840
|
-
return
|
|
1938
|
+
return name
|
|
1841
1939
|
|
|
1842
1940
|
#--------------------------------------------------
|
|
1843
1941
|
# Exec
|
|
@@ -2408,7 +2506,7 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
2408
2506
|
return None
|
|
2409
2507
|
return results[0][0]
|
|
2410
2508
|
|
|
2411
|
-
# CLI methods (list_warehouses, list_compute_pools, list_roles, list_apps,
|
|
2509
|
+
# CLI methods (list_warehouses, list_compute_pools, list_roles, list_apps,
|
|
2412
2510
|
# list_databases, list_sf_schemas, list_tables) are now in CLIResources class
|
|
2413
2511
|
# schema_info is kept in base Resources class since it's used by SnowflakeSchema._fetch_info()
|
|
2414
2512
|
|