relationalai 1.0.0a1__py3-none-any.whl → 1.0.0a3__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 (57) hide show
  1. relationalai/semantics/frontend/base.py +3 -0
  2. relationalai/semantics/frontend/front_compiler.py +5 -2
  3. relationalai/semantics/metamodel/builtins.py +2 -1
  4. relationalai/semantics/metamodel/metamodel.py +32 -4
  5. relationalai/semantics/metamodel/pprint.py +5 -3
  6. relationalai/semantics/metamodel/typer.py +324 -297
  7. relationalai/semantics/std/aggregates.py +0 -1
  8. relationalai/semantics/std/datetime.py +4 -1
  9. relationalai/shims/executor.py +26 -5
  10. relationalai/shims/mm2v0.py +119 -44
  11. {relationalai-1.0.0a1.dist-info → relationalai-1.0.0a3.dist-info}/METADATA +1 -1
  12. {relationalai-1.0.0a1.dist-info → relationalai-1.0.0a3.dist-info}/RECORD +57 -48
  13. v0/relationalai/__init__.py +69 -22
  14. v0/relationalai/clients/__init__.py +15 -2
  15. v0/relationalai/clients/client.py +4 -4
  16. v0/relationalai/clients/local.py +5 -5
  17. v0/relationalai/clients/resources/__init__.py +8 -0
  18. v0/relationalai/clients/{azure.py → resources/azure/azure.py} +12 -12
  19. v0/relationalai/clients/resources/snowflake/__init__.py +20 -0
  20. v0/relationalai/clients/resources/snowflake/cli_resources.py +87 -0
  21. v0/relationalai/clients/resources/snowflake/direct_access_resources.py +711 -0
  22. v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
  23. v0/relationalai/clients/resources/snowflake/error_handlers.py +199 -0
  24. v0/relationalai/clients/resources/snowflake/resources_factory.py +99 -0
  25. v0/relationalai/clients/{snowflake.py → resources/snowflake/snowflake.py} +606 -1392
  26. v0/relationalai/clients/{use_index_poller.py → resources/snowflake/use_index_poller.py} +43 -12
  27. v0/relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
  28. v0/relationalai/clients/resources/snowflake/util.py +387 -0
  29. v0/relationalai/early_access/dsl/ir/executor.py +4 -4
  30. v0/relationalai/early_access/dsl/snow/api.py +2 -1
  31. v0/relationalai/errors.py +23 -0
  32. v0/relationalai/experimental/solvers.py +7 -7
  33. v0/relationalai/semantics/devtools/benchmark_lqp.py +4 -5
  34. v0/relationalai/semantics/devtools/extract_lqp.py +1 -1
  35. v0/relationalai/semantics/internal/internal.py +4 -4
  36. v0/relationalai/semantics/internal/snowflake.py +3 -2
  37. v0/relationalai/semantics/lqp/executor.py +20 -22
  38. v0/relationalai/semantics/lqp/model2lqp.py +42 -4
  39. v0/relationalai/semantics/lqp/passes.py +1 -1
  40. v0/relationalai/semantics/lqp/rewrite/cdc.py +1 -1
  41. v0/relationalai/semantics/lqp/rewrite/extract_keys.py +53 -12
  42. v0/relationalai/semantics/metamodel/builtins.py +8 -6
  43. v0/relationalai/semantics/metamodel/rewrite/flatten.py +9 -4
  44. v0/relationalai/semantics/metamodel/util.py +6 -5
  45. v0/relationalai/semantics/reasoners/graph/core.py +8 -9
  46. v0/relationalai/semantics/rel/executor.py +14 -11
  47. v0/relationalai/semantics/sql/compiler.py +2 -2
  48. v0/relationalai/semantics/sql/executor/snowflake.py +9 -5
  49. v0/relationalai/semantics/tests/test_snapshot_abstract.py +1 -1
  50. v0/relationalai/tools/cli.py +26 -30
  51. v0/relationalai/tools/cli_helpers.py +10 -2
  52. v0/relationalai/util/otel_configuration.py +2 -1
  53. v0/relationalai/util/otel_handler.py +1 -1
  54. {relationalai-1.0.0a1.dist-info → relationalai-1.0.0a3.dist-info}/WHEEL +0 -0
  55. {relationalai-1.0.0a1.dist-info → relationalai-1.0.0a3.dist-info}/entry_points.txt +0 -0
  56. {relationalai-1.0.0a1.dist-info → relationalai-1.0.0a3.dist-info}/top_level.txt +0 -0
  57. /v0/relationalai/clients/{cache_store.py → resources/snowflake/cache_store.py} +0 -0
@@ -5,21 +5,23 @@ import json
5
5
  import logging
6
6
  import uuid
7
7
 
8
- from v0.relationalai import debugging
9
- from v0.relationalai.clients.cache_store import GraphIndexCache
10
- from v0.relationalai.clients.util import (
8
+ from .... import debugging
9
+ from .cache_store import GraphIndexCache
10
+ from .util import collect_error_messages
11
+ from ...util import (
11
12
  get_pyrel_version,
12
13
  normalize_datetime,
13
14
  poll_with_specified_overhead,
14
15
  )
15
- from v0.relationalai.errors import (
16
+ from ....errors import (
16
17
  ERPNotRunningError,
17
18
  EngineProvisioningFailed,
18
19
  SnowflakeChangeTrackingNotEnabledException,
19
20
  SnowflakeTableObjectsException,
20
21
  SnowflakeTableObject,
22
+ SnowflakeRaiAppNotStarted,
21
23
  )
22
- from v0.relationalai.tools.cli_controls import (
24
+ from ....tools.cli_controls import (
23
25
  DebuggingSpan,
24
26
  create_progress,
25
27
  TASK_CATEGORY_INDEXING,
@@ -30,7 +32,7 @@ from v0.relationalai.tools.cli_controls import (
30
32
  TASK_CATEGORY_STATUS,
31
33
  TASK_CATEGORY_VALIDATION,
32
34
  )
33
- from v0.relationalai.tools.constants import WAIT_FOR_STREAM_SYNC, Generation
35
+ from ....tools.constants import WAIT_FOR_STREAM_SYNC, Generation
34
36
 
35
37
  # Set up logger for this module
36
38
  logger = logging.getLogger(__name__)
@@ -44,8 +46,8 @@ except ImportError:
44
46
  Table = None
45
47
 
46
48
  if TYPE_CHECKING:
47
- from v0.relationalai.clients.snowflake import Resources
48
- from v0.relationalai.clients.snowflake import DirectAccessResources
49
+ from .snowflake import Resources
50
+ from .direct_access_resources import DirectAccessResources
49
51
 
50
52
  # Maximum number of items to show individual subtasks for
51
53
  # If more items than this, show a single summary subtask instead
@@ -278,7 +280,7 @@ class UseIndexPoller:
278
280
  Raises:
279
281
  ValueError: If the query fails (permissions, table doesn't exist, etc.)
280
282
  """
281
- from v0.relationalai.clients.snowflake import PYREL_ROOT_DB
283
+ from v0.relationalai.clients.resources.snowflake import PYREL_ROOT_DB
282
284
 
283
285
  # Build FQN list for SQL IN clause
284
286
  fqn_list = ", ".join([f"'{source}'" for source in sources])
@@ -427,7 +429,7 @@ class UseIndexPoller:
427
429
  return
428
430
 
429
431
  # Delete truly stale streams
430
- from v0.relationalai.clients.snowflake import PYREL_ROOT_DB
432
+ from v0.relationalai.clients.resources.snowflake import PYREL_ROOT_DB
431
433
  query = f"CALL {self.app_name}.api.delete_data_streams({truly_stale}, '{PYREL_ROOT_DB}');"
432
434
 
433
435
  self._add_deletion_subtasks(progress, truly_stale)
@@ -456,7 +458,8 @@ class UseIndexPoller:
456
458
  )
457
459
 
458
460
  # Don't raise if streams don't exist - this is expected
459
- if "data streams do not exist" not in str(e).lower():
461
+ messages = collect_error_messages(e)
462
+ if not any("data streams do not exist" in msg for msg in messages):
460
463
  raise e from None
461
464
 
462
465
  def _poll_loop(self, progress) -> None:
@@ -577,6 +580,9 @@ class UseIndexPoller:
577
580
  if fq_name in self.stream_task_ids and data.get("errors", []):
578
581
  for error in data.get("errors", []):
579
582
  error_msg = f"{error.get('error')}, source: {error.get('source')}"
583
+ # Some failures indicate the RAI app is not started/active; surface
584
+ # them as a rich, actionable error instead of aggregating.
585
+ self._raise_if_app_not_started(error_msg)
580
586
  self.table_objects_with_other_errors.append(
581
587
  SnowflakeTableObject(error_msg, fq_name)
582
588
  )
@@ -702,6 +708,7 @@ class UseIndexPoller:
702
708
  err_source_type = self.source_info.get(err_source, {}).get("type")
703
709
  self.tables_with_not_enabled_change_tracking.append((err_source, err_source_type))
704
710
  else:
711
+ self._raise_if_app_not_started(error.get("message", ""))
705
712
  self.table_objects_with_other_errors.append(
706
713
  SnowflakeTableObject(error.get("message"), error.get("source"))
707
714
  )
@@ -709,6 +716,7 @@ class UseIndexPoller:
709
716
  self.engine_errors.append(error)
710
717
  else:
711
718
  # Other types of errors, e.g. "validation"
719
+ self._raise_if_app_not_started(error.get("message", ""))
712
720
  self.table_objects_with_other_errors.append(
713
721
  SnowflakeTableObject(error.get("message"), error.get("source"))
714
722
  )
@@ -737,6 +745,29 @@ class UseIndexPoller:
737
745
 
738
746
  poll_with_specified_overhead(lambda: check_ready(progress), overhead_rate=POLL_OVERHEAD_RATE, max_delay=POLL_MAX_DELAY)
739
747
 
748
+ def _raise_if_app_not_started(self, message: str) -> None:
749
+ """Detect Snowflake-side 'app not active / service not started' messages and raise a rich exception.
750
+
751
+ The use_index stored procedure reports many failures inside the returned JSON payload
752
+ (use_index_data['errors']) rather than raising them as Snowflake exceptions, so the
753
+ standard `_exec()` error handlers won't run. We detect the known activation-needed
754
+ signals here and raise `SnowflakeRaiAppNotStarted` for nicer formatting.
755
+ """
756
+ if not message:
757
+ return
758
+ msg = str(message).lower()
759
+ if (
760
+ "service has not been started" in msg
761
+ or "call app.activate()" in msg
762
+ or "app_not_active_exception" in msg
763
+ or "application is not active" in msg
764
+ or "use the app.activate()" in msg
765
+ ):
766
+ app_name = self.res.config.get("rai_app_name", "") if hasattr(self.res, "config") else ""
767
+ if not isinstance(app_name, str) or not app_name:
768
+ app_name = self.app_name
769
+ raise SnowflakeRaiAppNotStarted(app_name)
770
+
740
771
  def _post_check(self, progress) -> None:
741
772
  """Run post-processing checks including change tracking enablement.
742
773
 
@@ -887,7 +918,7 @@ class DirectUseIndexPoller(UseIndexPoller):
887
918
  headers=headers,
888
919
  generation=generation,
889
920
  )
890
- from v0.relationalai.clients.snowflake import DirectAccessResources
921
+ from v0.relationalai.clients.resources.snowflake import DirectAccessResources
891
922
  self.res: DirectAccessResources = cast(DirectAccessResources, self.res)
892
923
 
893
924
  def poll(self) -> None:
@@ -0,0 +1,188 @@
1
+ """
2
+ Use Index Resources - Resources class with use_index functionality.
3
+ This class keeps the use_index retry logic in _exec and provides use_index methods.
4
+ """
5
+ from __future__ import annotations
6
+ from typing import Iterable, Dict, Any
7
+
8
+ from .use_index_poller import UseIndexPoller
9
+ from ...config import Config
10
+ from ....tools.constants import Generation
11
+ from snowflake.snowpark import Session
12
+ from .error_handlers import ErrorHandler, UseIndexRetryErrorHandler
13
+
14
+ # Import Resources from snowflake - this creates a dependency but no circular import
15
+ # since snowflake.py doesn't import from this file
16
+ from .snowflake import Resources
17
+ from .util import (
18
+ is_engine_issue as _is_engine_issue,
19
+ is_database_issue as _is_database_issue,
20
+ collect_error_messages,
21
+ )
22
+
23
+
24
+ class UseIndexResources(Resources):
25
+ """
26
+ Resources class with use_index functionality.
27
+ Provides use_index polling methods and keeps use_index retry logic in _exec.
28
+ """
29
+ def __init__(
30
+ self,
31
+ profile: str | None = None,
32
+ config: Config | None = None,
33
+ connection: Session | None = None,
34
+ dry_run: bool = False,
35
+ reset_session: bool = False,
36
+ generation: Generation | None = None,
37
+ language: str = "rel",
38
+ ):
39
+ super().__init__(
40
+ profile=profile,
41
+ config=config,
42
+ connection=connection,
43
+ dry_run=dry_run,
44
+ reset_session=reset_session,
45
+ generation=generation,
46
+ )
47
+ self.database = ""
48
+ self.language = language
49
+
50
+ def _is_db_or_engine_error(self, e: Exception) -> bool:
51
+ """Check if an exception indicates a database or engine error."""
52
+ messages = collect_error_messages(e)
53
+ for msg in messages:
54
+ if msg and (_is_database_issue(msg) or _is_engine_issue(msg)):
55
+ return True
56
+ return False
57
+
58
+ def _get_error_handlers(self) -> list[ErrorHandler]:
59
+ # Ensure use_index retry happens before standard database/engine error handlers.
60
+ return [UseIndexRetryErrorHandler(), *super()._get_error_handlers()]
61
+
62
+ def _poll_use_index(
63
+ self,
64
+ app_name: str,
65
+ sources: Iterable[str],
66
+ model: str,
67
+ engine_name: str,
68
+ engine_size: str | None = None,
69
+ program_span_id: str | None = None,
70
+ headers: Dict | None = None,
71
+ ):
72
+ """Poll use_index to prepare indices for the given sources."""
73
+ return UseIndexPoller(
74
+ self,
75
+ app_name,
76
+ sources,
77
+ model,
78
+ engine_name,
79
+ engine_size,
80
+ self.language,
81
+ program_span_id,
82
+ headers,
83
+ self.generation
84
+ ).poll()
85
+
86
+ def maybe_poll_use_index(
87
+ self,
88
+ app_name: str,
89
+ sources: Iterable[str],
90
+ model: str,
91
+ engine_name: str,
92
+ engine_size: str | None = None,
93
+ program_span_id: str | None = None,
94
+ headers: Dict | None = None,
95
+ ):
96
+ """Only call poll() if there are sources to process and cache is not valid."""
97
+ sources_list = list(sources)
98
+ self.database = model
99
+ if sources_list:
100
+ poller = UseIndexPoller(
101
+ self,
102
+ app_name,
103
+ sources_list,
104
+ model,
105
+ engine_name,
106
+ engine_size,
107
+ self.language,
108
+ program_span_id,
109
+ headers,
110
+ self.generation
111
+ )
112
+ # If cache is valid (data freshness has not expired), skip polling
113
+ if poller.cache.is_valid():
114
+ cached_sources = len(poller.cache.sources)
115
+ total_sources = len(sources_list)
116
+ cached_timestamp = poller.cache._metadata.get("cachedIndices", {}).get(poller.cache.key, {}).get("last_use_index_update_on", "")
117
+
118
+ message = f"Using cached data for {cached_sources}/{total_sources} data streams"
119
+ if cached_timestamp:
120
+ print(f"\n{message} (cached at {cached_timestamp})\n")
121
+ else:
122
+ print(f"\n{message}\n")
123
+ else:
124
+ return poller.poll()
125
+
126
+ def _exec_with_gi_retry(
127
+ self,
128
+ database: str,
129
+ engine: str | None,
130
+ raw_code: str,
131
+ inputs: Dict | None,
132
+ readonly: bool,
133
+ nowait_durable: bool,
134
+ headers: Dict | None,
135
+ bypass_index: bool,
136
+ language: str,
137
+ query_timeout_mins: int | None,
138
+ ):
139
+ """Execute with graph index retry logic.
140
+
141
+ Attempts execution with gi_setup_skipped=True first. If an engine or database
142
+ issue occurs, polls use_index and retries with gi_setup_skipped=False.
143
+ """
144
+ try:
145
+ return self._exec_async_v2(
146
+ database, engine, raw_code, inputs, readonly, nowait_durable,
147
+ headers=headers, bypass_index=bypass_index, language=language,
148
+ query_timeout_mins=query_timeout_mins, gi_setup_skipped=True,
149
+ )
150
+ except Exception as e:
151
+ if not self._is_db_or_engine_error(e):
152
+ raise e
153
+
154
+ engine_name = engine or self.get_default_engine_name()
155
+ engine_size = self.config.get_default_engine_size()
156
+ self._poll_use_index(
157
+ app_name=self.get_app_name(),
158
+ sources=self.sources,
159
+ model=database,
160
+ engine_name=engine_name,
161
+ engine_size=engine_size,
162
+ headers=headers,
163
+ )
164
+
165
+ return self._exec_async_v2(
166
+ database, engine, raw_code, inputs, readonly, nowait_durable,
167
+ headers=headers, bypass_index=bypass_index, language=language,
168
+ query_timeout_mins=query_timeout_mins, gi_setup_skipped=False,
169
+ )
170
+
171
+ def _execute_code(
172
+ self,
173
+ database: str,
174
+ engine: str | None,
175
+ raw_code: str,
176
+ inputs: Dict | None,
177
+ readonly: bool,
178
+ nowait_durable: bool,
179
+ headers: Dict | None,
180
+ bypass_index: bool,
181
+ language: str,
182
+ query_timeout_mins: int | None,
183
+ ) -> Any:
184
+ """Override to use retry logic with use_index polling."""
185
+ return self._exec_with_gi_retry(
186
+ database, engine, raw_code, inputs, readonly, nowait_durable,
187
+ headers, bypass_index, language, query_timeout_mins
188
+ )