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.
Files changed (118) hide show
  1. relationalai/config/config.py +47 -21
  2. relationalai/config/connections/__init__.py +5 -2
  3. relationalai/config/connections/duckdb.py +2 -2
  4. relationalai/config/connections/local.py +31 -0
  5. relationalai/config/connections/snowflake.py +0 -1
  6. relationalai/config/external/raiconfig_converter.py +235 -0
  7. relationalai/config/external/raiconfig_models.py +202 -0
  8. relationalai/config/external/utils.py +31 -0
  9. relationalai/config/shims.py +1 -0
  10. relationalai/semantics/__init__.py +10 -8
  11. relationalai/semantics/backends/sql/sql_compiler.py +1 -4
  12. relationalai/semantics/experimental/__init__.py +0 -0
  13. relationalai/semantics/experimental/builder.py +295 -0
  14. relationalai/semantics/experimental/builtins.py +154 -0
  15. relationalai/semantics/frontend/base.py +67 -42
  16. relationalai/semantics/frontend/core.py +34 -6
  17. relationalai/semantics/frontend/front_compiler.py +209 -37
  18. relationalai/semantics/frontend/pprint.py +6 -2
  19. relationalai/semantics/metamodel/__init__.py +7 -0
  20. relationalai/semantics/metamodel/metamodel.py +2 -0
  21. relationalai/semantics/metamodel/metamodel_analyzer.py +58 -16
  22. relationalai/semantics/metamodel/pprint.py +6 -1
  23. relationalai/semantics/metamodel/rewriter.py +11 -7
  24. relationalai/semantics/metamodel/typer.py +116 -41
  25. relationalai/semantics/reasoners/__init__.py +11 -0
  26. relationalai/semantics/reasoners/graph/__init__.py +35 -0
  27. relationalai/semantics/reasoners/graph/core.py +9028 -0
  28. relationalai/semantics/std/__init__.py +30 -10
  29. relationalai/semantics/std/aggregates.py +641 -12
  30. relationalai/semantics/std/common.py +146 -13
  31. relationalai/semantics/std/constraints.py +71 -1
  32. relationalai/semantics/std/datetime.py +904 -21
  33. relationalai/semantics/std/decimals.py +143 -2
  34. relationalai/semantics/std/floats.py +57 -4
  35. relationalai/semantics/std/integers.py +98 -4
  36. relationalai/semantics/std/math.py +857 -35
  37. relationalai/semantics/std/numbers.py +216 -20
  38. relationalai/semantics/std/re.py +213 -5
  39. relationalai/semantics/std/strings.py +437 -44
  40. relationalai/shims/executor.py +60 -52
  41. relationalai/shims/fixtures.py +85 -0
  42. relationalai/shims/helpers.py +26 -2
  43. relationalai/shims/hoister.py +28 -9
  44. relationalai/shims/mm2v0.py +204 -173
  45. relationalai/tools/cli/cli.py +192 -10
  46. relationalai/tools/cli/components/progress_reader.py +1 -1
  47. relationalai/tools/cli/docs.py +394 -0
  48. relationalai/tools/debugger.py +11 -4
  49. relationalai/tools/qb_debugger.py +435 -0
  50. relationalai/tools/typer_debugger.py +1 -2
  51. relationalai/util/dataclasses.py +3 -5
  52. relationalai/util/docutils.py +1 -2
  53. relationalai/util/error.py +2 -5
  54. relationalai/util/python.py +23 -0
  55. relationalai/util/runtime.py +1 -2
  56. relationalai/util/schema.py +2 -4
  57. relationalai/util/structures.py +4 -2
  58. relationalai/util/tracing.py +8 -2
  59. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/METADATA +8 -5
  60. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/RECORD +118 -95
  61. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/WHEEL +1 -1
  62. v0/relationalai/__init__.py +1 -1
  63. v0/relationalai/clients/client.py +52 -18
  64. v0/relationalai/clients/exec_txn_poller.py +122 -0
  65. v0/relationalai/clients/local.py +23 -8
  66. v0/relationalai/clients/resources/azure/azure.py +36 -11
  67. v0/relationalai/clients/resources/snowflake/__init__.py +4 -4
  68. v0/relationalai/clients/resources/snowflake/cli_resources.py +12 -1
  69. v0/relationalai/clients/resources/snowflake/direct_access_resources.py +124 -100
  70. v0/relationalai/clients/resources/snowflake/engine_service.py +381 -0
  71. v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +35 -29
  72. v0/relationalai/clients/resources/snowflake/error_handlers.py +43 -2
  73. v0/relationalai/clients/resources/snowflake/snowflake.py +277 -179
  74. v0/relationalai/clients/resources/snowflake/use_index_poller.py +8 -0
  75. v0/relationalai/clients/types.py +5 -0
  76. v0/relationalai/errors.py +19 -1
  77. v0/relationalai/semantics/lqp/algorithms.py +173 -0
  78. v0/relationalai/semantics/lqp/builtins.py +199 -2
  79. v0/relationalai/semantics/lqp/executor.py +68 -37
  80. v0/relationalai/semantics/lqp/ir.py +28 -2
  81. v0/relationalai/semantics/lqp/model2lqp.py +215 -45
  82. v0/relationalai/semantics/lqp/passes.py +13 -658
  83. v0/relationalai/semantics/lqp/rewrite/__init__.py +12 -0
  84. v0/relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
  85. v0/relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
  86. v0/relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
  87. v0/relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
  88. v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
  89. v0/relationalai/semantics/lqp/rewrite/period_math.py +77 -0
  90. v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
  91. v0/relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
  92. v0/relationalai/semantics/lqp/utils.py +11 -1
  93. v0/relationalai/semantics/lqp/validators.py +14 -1
  94. v0/relationalai/semantics/metamodel/builtins.py +2 -1
  95. v0/relationalai/semantics/metamodel/compiler.py +2 -1
  96. v0/relationalai/semantics/metamodel/dependency.py +12 -3
  97. v0/relationalai/semantics/metamodel/executor.py +11 -1
  98. v0/relationalai/semantics/metamodel/factory.py +2 -2
  99. v0/relationalai/semantics/metamodel/helpers.py +7 -0
  100. v0/relationalai/semantics/metamodel/ir.py +3 -2
  101. v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
  102. v0/relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
  103. v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
  104. v0/relationalai/semantics/metamodel/typer/checker.py +6 -4
  105. v0/relationalai/semantics/metamodel/typer/typer.py +4 -3
  106. v0/relationalai/semantics/metamodel/visitor.py +4 -3
  107. v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
  108. v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +336 -86
  109. v0/relationalai/semantics/rel/compiler.py +2 -1
  110. v0/relationalai/semantics/rel/executor.py +3 -2
  111. v0/relationalai/semantics/tests/lqp/__init__.py +0 -0
  112. v0/relationalai/semantics/tests/lqp/algorithms.py +345 -0
  113. v0/relationalai/tools/cli.py +339 -186
  114. v0/relationalai/tools/cli_controls.py +216 -67
  115. v0/relationalai/tools/cli_helpers.py +410 -6
  116. v0/relationalai/util/format.py +5 -2
  117. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/entry_points.txt +0 -0
  118. {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
- sizes = []
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 get_engine(self, name: str):
722
- results = self._exec(
723
- f"SELECT NAME, ID, SIZE, STATUS, CREATED_BY, CREATED_ON, UPDATED_ON, VERSION, AUTO_SUSPEND_MINS, SUSPENDS_AT FROM {APP_NAME}.api.engines WHERE NAME='{name}';"
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
- if not results:
726
- return None
727
- engine = results[0]
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
- api = "create_engine_async" if is_async else "create_engine"
772
- if size is None:
773
- size = self.config.get_default_engine_size()
774
- # if auto_suspend_mins is None, get the default value from the config
775
- if auto_suspend_mins is None:
776
- auto_suspend_mins = self.config.get_default_auto_suspend_mins()
777
- try:
778
- headers = debugging.gen_current_propagation_headers()
779
- with debugging.span(api, name=name, size=size, auto_suspend_mins=auto_suspend_mins):
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(self, name:str, size:str|None=None, auto_suspend_mins:int|None=None, headers: Dict | None = None):
789
- self._create_engine(name, size, auto_suspend_mins, headers=headers)
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(self, name:str, size:str|None=None, auto_suspend_mins:int|None=None):
792
- self._create_engine(name, size, auto_suspend_mins, True)
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, force:bool = False, headers: Dict | None = None):
795
- request_headers = debugging.add_current_propagation_headers(headers)
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._exec(f"call {APP_NAME}.api.suspend_engine('{name}');")
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
- request_headers = debugging.add_current_propagation_headers(headers)
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
- if headers is None:
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._exec(f"call {APP_NAME}.api.alter_engine_pool_node_limits('{size}', {mins}, {maxs});")
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" and response_row.get("ABORT_REASON", "") == TXN_ABORT_REASON_TIMEOUT:
1415
- config_file_path = getattr(self.config, 'file_path', None)
1416
- # todo: use the timeout returned alongside the transaction as soon as it's exposed
1417
- timeout_mins = int(self.config.get("query_timeout_mins", DEFAULT_QUERY_TIMEOUT_MINS) or DEFAULT_QUERY_TIMEOUT_MINS)
1418
- raise QueryTimeoutExceededException(
1419
- timeout_mins=timeout_mins,
1420
- query_id=txn_id,
1421
- config_file_path=config_file_path,
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 _exec_rai_app
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 can be
1683
- overridden by child classes (e.g., DirectAccessResources uses HTTP instead).
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
- with debugging.span("create_v2", **query_attrs_dict) as create_span:
1708
- request_headers['user-agent'] = get_pyrel_version(self.generation)
1709
- request_headers['gi_setup_skipped'] = str(gi_setup_skipped)
1710
- request_headers['pyrel_program_id'] = debugging.get_program_span_id() or ""
1711
- response = self._exec_rai_app(
1712
- database=database,
1713
- engine=engine,
1714
- raw_code=raw_code,
1715
- inputs=inputs,
1716
- readonly=readonly,
1717
- nowait_durable=nowait_durable,
1718
- request_headers=request_headers,
1719
- bypass_index=bypass_index,
1720
- language=language,
1721
- query_timeout_mins=query_timeout_mins,
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
- artifact_info = {}
1725
- rows = list(iter(response))
1818
+ txn_id = result.txn_id
1819
+ state = result.state
1726
1820
 
1727
- # process the first row since txn_id and state are the same for all rows
1728
- first_row = rows[0]
1729
- txn_id = first_row['ID']
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
- txn_span["txn_id"] = txn_id
1734
- create_span["txn_id"] = txn_id
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
- # fast path: transaction already finished
1738
- if state in ["COMPLETED", "ABORTED"]:
1739
- if txn_id in self._pending_transactions:
1740
- self._pending_transactions.remove(txn_id)
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
- # Process rows to get the rest of the artifacts
1743
- for row in rows:
1744
- filename = row['FILENAME']
1745
- artifact_info[filename] = row
1833
+ artifact_info = result.artifact_info
1746
1834
 
1747
- # Slow path: transaction not done yet; start polling
1748
- else:
1749
- self._pending_transactions.append(txn_id)
1750
- with debugging.span("wait", txn_id=txn_id):
1751
- poll_with_specified_overhead(
1752
- lambda: self._check_exec_async_status(txn_id, headers=request_headers), 0.1
1753
- )
1754
- artifact_info = self._list_exec_async_artifacts(txn_id, headers=request_headers)
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
- with debugging.span("fetch"):
1757
- return self._download_results(artifact_info, txn_id, state)
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(self, name: str | None = None, size: str | None = None, headers: Dict | None = None):
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
- engine_name, engine_size = self._prepare_engine_params(name, size)
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(engine_name)
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
- engine_name=engine_name,
1790
- engine_size=engine_size,
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, engine_name)
1895
+ self._handle_engine_creation_errors(e, name)
1801
1896
 
1802
- return engine_name
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
- engine_name, engine_size = self._prepare_engine_params(name, None, use_default_size=True)
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(engine_name)
1917
+ engine = self.get_engine(name, type)
1821
1918
 
1822
1919
  # Create context for state handling
1823
1920
  context = EngineContext(
1824
- engine_name=engine_name,
1825
- engine_size=engine_size,
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 {engine_name}",
1934
+ "finished_message": f"Failed to create engine {name}",
1837
1935
  })
1838
- self._handle_engine_creation_errors(e, engine_name, preserve_rai_exception=True)
1936
+ self._handle_engine_creation_errors(e, name, preserve_rai_exception=True)
1839
1937
 
1840
- return engine_name
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