relationalai 1.0.0a2__py3-none-any.whl → 1.0.0a4__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/config/shims.py +1 -0
  2. relationalai/semantics/__init__.py +7 -1
  3. relationalai/semantics/frontend/base.py +19 -13
  4. relationalai/semantics/frontend/core.py +30 -2
  5. relationalai/semantics/frontend/front_compiler.py +38 -11
  6. relationalai/semantics/frontend/pprint.py +1 -1
  7. relationalai/semantics/metamodel/rewriter.py +6 -2
  8. relationalai/semantics/metamodel/typer.py +70 -26
  9. relationalai/semantics/reasoners/__init__.py +11 -0
  10. relationalai/semantics/reasoners/graph/__init__.py +38 -0
  11. relationalai/semantics/reasoners/graph/core.py +9015 -0
  12. relationalai/shims/executor.py +4 -1
  13. relationalai/shims/hoister.py +9 -0
  14. relationalai/shims/mm2v0.py +47 -34
  15. relationalai/tools/cli/cli.py +138 -0
  16. relationalai/tools/cli/docs.py +394 -0
  17. {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/METADATA +5 -3
  18. {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/RECORD +57 -43
  19. v0/relationalai/__init__.py +69 -22
  20. v0/relationalai/clients/__init__.py +15 -2
  21. v0/relationalai/clients/client.py +4 -4
  22. v0/relationalai/clients/exec_txn_poller.py +91 -0
  23. v0/relationalai/clients/local.py +5 -5
  24. v0/relationalai/clients/resources/__init__.py +8 -0
  25. v0/relationalai/clients/{azure.py → resources/azure/azure.py} +12 -12
  26. v0/relationalai/clients/resources/snowflake/__init__.py +20 -0
  27. v0/relationalai/clients/resources/snowflake/cli_resources.py +87 -0
  28. v0/relationalai/clients/resources/snowflake/direct_access_resources.py +717 -0
  29. v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
  30. v0/relationalai/clients/resources/snowflake/error_handlers.py +199 -0
  31. v0/relationalai/clients/resources/snowflake/resources_factory.py +99 -0
  32. v0/relationalai/clients/{snowflake.py → resources/snowflake/snowflake.py} +642 -1399
  33. v0/relationalai/clients/{use_index_poller.py → resources/snowflake/use_index_poller.py} +51 -12
  34. v0/relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
  35. v0/relationalai/clients/resources/snowflake/util.py +387 -0
  36. v0/relationalai/early_access/dsl/ir/executor.py +4 -4
  37. v0/relationalai/early_access/dsl/snow/api.py +2 -1
  38. v0/relationalai/errors.py +18 -0
  39. v0/relationalai/experimental/solvers.py +7 -7
  40. v0/relationalai/semantics/devtools/benchmark_lqp.py +4 -5
  41. v0/relationalai/semantics/devtools/extract_lqp.py +1 -1
  42. v0/relationalai/semantics/internal/snowflake.py +1 -1
  43. v0/relationalai/semantics/lqp/executor.py +7 -12
  44. v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
  45. v0/relationalai/semantics/metamodel/util.py +6 -5
  46. v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +335 -84
  47. v0/relationalai/semantics/rel/executor.py +14 -11
  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.0a2.dist-info → relationalai-1.0.0a4.dist-info}/WHEEL +0 -0
  55. {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/entry_points.txt +0 -0
  56. {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.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
@@ -187,6 +189,9 @@ class UseIndexPoller:
187
189
  # on every 5th iteration we reset the cdc status, so it will be checked again
188
190
  self.should_check_cdc = True
189
191
 
192
+ # Flag to only check data stream health once in the first call
193
+ self.check_data_stream_health = True
194
+
190
195
  self.wait_for_stream_sync = self.res.config.get(
191
196
  "wait_for_stream_sync", WAIT_FOR_STREAM_SYNC
192
197
  )
@@ -278,7 +283,7 @@ class UseIndexPoller:
278
283
  Raises:
279
284
  ValueError: If the query fails (permissions, table doesn't exist, etc.)
280
285
  """
281
- from v0.relationalai.clients.snowflake import PYREL_ROOT_DB
286
+ from v0.relationalai.clients.resources.snowflake import PYREL_ROOT_DB
282
287
 
283
288
  # Build FQN list for SQL IN clause
284
289
  fqn_list = ", ".join([f"'{source}'" for source in sources])
@@ -427,7 +432,7 @@ class UseIndexPoller:
427
432
  return
428
433
 
429
434
  # Delete truly stale streams
430
- from v0.relationalai.clients.snowflake import PYREL_ROOT_DB
435
+ from v0.relationalai.clients.resources.snowflake import PYREL_ROOT_DB
431
436
  query = f"CALL {self.app_name}.api.delete_data_streams({truly_stale}, '{PYREL_ROOT_DB}');"
432
437
 
433
438
  self._add_deletion_subtasks(progress, truly_stale)
@@ -456,7 +461,8 @@ class UseIndexPoller:
456
461
  )
457
462
 
458
463
  # Don't raise if streams don't exist - this is expected
459
- if "data streams do not exist" not in str(e).lower():
464
+ messages = collect_error_messages(e)
465
+ if not any("data streams do not exist" in msg for msg in messages):
460
466
  raise e from None
461
467
 
462
468
  def _poll_loop(self, progress) -> None:
@@ -500,6 +506,7 @@ class UseIndexPoller:
500
506
  "init_engine_async": self.init_engine_async,
501
507
  "language": self.language,
502
508
  "data_freshness_mins": self.data_freshness,
509
+ "check_data_stream_health": self.check_data_stream_health
503
510
  })
504
511
 
505
512
  request_headers = debugging.add_current_propagation_headers(self.headers)
@@ -532,6 +539,7 @@ class UseIndexPoller:
532
539
  errors = use_index_data.get("errors", [])
533
540
  relations = use_index_data.get("relations", {})
534
541
  cdc_enabled = use_index_data.get("cdcEnabled", False)
542
+ health_checked = use_index_data.get("healthChecked", False)
535
543
  if self.check_ready_count % ERP_CHECK_FREQUENCY == 0 or not cdc_enabled:
536
544
  self.should_check_cdc = True
537
545
  else:
@@ -539,6 +547,9 @@ class UseIndexPoller:
539
547
 
540
548
  if engines and self.init_engine_async:
541
549
  self.init_engine_async = False
550
+
551
+ if self.check_data_stream_health and health_checked:
552
+ self.check_data_stream_health = False
542
553
 
543
554
  break_loop = False
544
555
  has_stream_errors = False
@@ -577,6 +588,9 @@ class UseIndexPoller:
577
588
  if fq_name in self.stream_task_ids and data.get("errors", []):
578
589
  for error in data.get("errors", []):
579
590
  error_msg = f"{error.get('error')}, source: {error.get('source')}"
591
+ # Some failures indicate the RAI app is not started/active; surface
592
+ # them as a rich, actionable error instead of aggregating.
593
+ self._raise_if_app_not_started(error_msg)
580
594
  self.table_objects_with_other_errors.append(
581
595
  SnowflakeTableObject(error_msg, fq_name)
582
596
  )
@@ -702,6 +716,7 @@ class UseIndexPoller:
702
716
  err_source_type = self.source_info.get(err_source, {}).get("type")
703
717
  self.tables_with_not_enabled_change_tracking.append((err_source, err_source_type))
704
718
  else:
719
+ self._raise_if_app_not_started(error.get("message", ""))
705
720
  self.table_objects_with_other_errors.append(
706
721
  SnowflakeTableObject(error.get("message"), error.get("source"))
707
722
  )
@@ -709,6 +724,7 @@ class UseIndexPoller:
709
724
  self.engine_errors.append(error)
710
725
  else:
711
726
  # Other types of errors, e.g. "validation"
727
+ self._raise_if_app_not_started(error.get("message", ""))
712
728
  self.table_objects_with_other_errors.append(
713
729
  SnowflakeTableObject(error.get("message"), error.get("source"))
714
730
  )
@@ -737,6 +753,29 @@ class UseIndexPoller:
737
753
 
738
754
  poll_with_specified_overhead(lambda: check_ready(progress), overhead_rate=POLL_OVERHEAD_RATE, max_delay=POLL_MAX_DELAY)
739
755
 
756
+ def _raise_if_app_not_started(self, message: str) -> None:
757
+ """Detect Snowflake-side 'app not active / service not started' messages and raise a rich exception.
758
+
759
+ The use_index stored procedure reports many failures inside the returned JSON payload
760
+ (use_index_data['errors']) rather than raising them as Snowflake exceptions, so the
761
+ standard `_exec()` error handlers won't run. We detect the known activation-needed
762
+ signals here and raise `SnowflakeRaiAppNotStarted` for nicer formatting.
763
+ """
764
+ if not message:
765
+ return
766
+ msg = str(message).lower()
767
+ if (
768
+ "service has not been started" in msg
769
+ or "call app.activate()" in msg
770
+ or "app_not_active_exception" in msg
771
+ or "application is not active" in msg
772
+ or "use the app.activate()" in msg
773
+ ):
774
+ app_name = self.res.config.get("rai_app_name", "") if hasattr(self.res, "config") else ""
775
+ if not isinstance(app_name, str) or not app_name:
776
+ app_name = self.app_name
777
+ raise SnowflakeRaiAppNotStarted(app_name)
778
+
740
779
  def _post_check(self, progress) -> None:
741
780
  """Run post-processing checks including change tracking enablement.
742
781
 
@@ -887,7 +926,7 @@ class DirectUseIndexPoller(UseIndexPoller):
887
926
  headers=headers,
888
927
  generation=generation,
889
928
  )
890
- from v0.relationalai.clients.snowflake import DirectAccessResources
929
+ from v0.relationalai.clients.resources.snowflake import DirectAccessResources
891
930
  self.res: DirectAccessResources = cast(DirectAccessResources, self.res)
892
931
 
893
932
  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
+ )