relationalai 0.12.13__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relationalai/__init__.py +69 -22
- relationalai/clients/__init__.py +15 -2
- relationalai/clients/client.py +4 -4
- relationalai/clients/local.py +5 -5
- relationalai/clients/resources/__init__.py +8 -0
- relationalai/clients/{azure.py → resources/azure/azure.py} +12 -12
- relationalai/clients/resources/snowflake/__init__.py +20 -0
- relationalai/clients/resources/snowflake/cli_resources.py +87 -0
- relationalai/clients/resources/snowflake/direct_access_resources.py +711 -0
- relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
- relationalai/clients/resources/snowflake/error_handlers.py +199 -0
- relationalai/clients/{export_procedure.py.jinja → resources/snowflake/export_procedure.py.jinja} +1 -1
- relationalai/clients/resources/snowflake/resources_factory.py +99 -0
- relationalai/clients/{snowflake.py → resources/snowflake/snowflake.py} +606 -1392
- relationalai/clients/{use_index_poller.py → resources/snowflake/use_index_poller.py} +43 -12
- relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
- relationalai/clients/resources/snowflake/util.py +387 -0
- relationalai/early_access/dsl/ir/executor.py +4 -4
- relationalai/early_access/dsl/snow/api.py +2 -1
- relationalai/errors.py +23 -0
- relationalai/experimental/solvers.py +7 -7
- relationalai/semantics/devtools/benchmark_lqp.py +4 -5
- relationalai/semantics/devtools/extract_lqp.py +1 -1
- relationalai/semantics/internal/internal.py +4 -4
- relationalai/semantics/internal/snowflake.py +3 -2
- relationalai/semantics/lqp/executor.py +22 -22
- relationalai/semantics/lqp/model2lqp.py +42 -4
- relationalai/semantics/lqp/passes.py +1 -1
- relationalai/semantics/lqp/rewrite/cdc.py +1 -1
- relationalai/semantics/lqp/rewrite/extract_keys.py +72 -15
- relationalai/semantics/metamodel/builtins.py +8 -6
- relationalai/semantics/metamodel/rewrite/flatten.py +9 -4
- relationalai/semantics/metamodel/util.py +6 -5
- relationalai/semantics/reasoners/graph/core.py +8 -9
- relationalai/semantics/rel/executor.py +14 -11
- relationalai/semantics/sql/compiler.py +2 -2
- relationalai/semantics/sql/executor/snowflake.py +9 -5
- relationalai/semantics/tests/test_snapshot_abstract.py +1 -1
- relationalai/tools/cli.py +26 -30
- relationalai/tools/cli_helpers.py +10 -2
- relationalai/util/otel_configuration.py +2 -1
- relationalai/util/otel_handler.py +1 -1
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/METADATA +1 -1
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/RECORD +49 -40
- relationalai_test_util/fixtures.py +2 -1
- /relationalai/clients/{cache_store.py → resources/snowflake/cache_store.py} +0 -0
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/WHEEL +0 -0
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/entry_points.txt +0 -0
- {relationalai-0.12.13.dist-info → relationalai-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,21 +5,23 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import uuid
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
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
|
|
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
|
|
24
|
+
from ....tools.cli_controls import (
|
|
23
25
|
DebuggingSpan,
|
|
24
26
|
create_progress,
|
|
25
27
|
TASK_CATEGORY_INDEXING,
|
|
@@ -30,7 +32,7 @@ from relationalai.tools.cli_controls import (
|
|
|
30
32
|
TASK_CATEGORY_STATUS,
|
|
31
33
|
TASK_CATEGORY_VALIDATION,
|
|
32
34
|
)
|
|
33
|
-
from
|
|
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
|
|
48
|
-
from
|
|
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 relationalai.clients.snowflake import PYREL_ROOT_DB
|
|
283
|
+
from 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 relationalai.clients.snowflake import PYREL_ROOT_DB
|
|
432
|
+
from 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
|
-
|
|
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 relationalai.clients.snowflake import DirectAccessResources
|
|
921
|
+
from 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
|
+
)
|