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.
- relationalai/config/shims.py +1 -0
- relationalai/semantics/__init__.py +7 -1
- relationalai/semantics/frontend/base.py +19 -13
- relationalai/semantics/frontend/core.py +30 -2
- relationalai/semantics/frontend/front_compiler.py +38 -11
- relationalai/semantics/frontend/pprint.py +1 -1
- relationalai/semantics/metamodel/rewriter.py +6 -2
- relationalai/semantics/metamodel/typer.py +70 -26
- relationalai/semantics/reasoners/__init__.py +11 -0
- relationalai/semantics/reasoners/graph/__init__.py +38 -0
- relationalai/semantics/reasoners/graph/core.py +9015 -0
- relationalai/shims/executor.py +4 -1
- relationalai/shims/hoister.py +9 -0
- relationalai/shims/mm2v0.py +47 -34
- relationalai/tools/cli/cli.py +138 -0
- relationalai/tools/cli/docs.py +394 -0
- {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/METADATA +5 -3
- {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/RECORD +57 -43
- v0/relationalai/__init__.py +69 -22
- v0/relationalai/clients/__init__.py +15 -2
- v0/relationalai/clients/client.py +4 -4
- v0/relationalai/clients/exec_txn_poller.py +91 -0
- v0/relationalai/clients/local.py +5 -5
- v0/relationalai/clients/resources/__init__.py +8 -0
- v0/relationalai/clients/{azure.py → resources/azure/azure.py} +12 -12
- v0/relationalai/clients/resources/snowflake/__init__.py +20 -0
- v0/relationalai/clients/resources/snowflake/cli_resources.py +87 -0
- v0/relationalai/clients/resources/snowflake/direct_access_resources.py +717 -0
- v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
- v0/relationalai/clients/resources/snowflake/error_handlers.py +199 -0
- v0/relationalai/clients/resources/snowflake/resources_factory.py +99 -0
- v0/relationalai/clients/{snowflake.py → resources/snowflake/snowflake.py} +642 -1399
- v0/relationalai/clients/{use_index_poller.py → resources/snowflake/use_index_poller.py} +51 -12
- v0/relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
- v0/relationalai/clients/resources/snowflake/util.py +387 -0
- v0/relationalai/early_access/dsl/ir/executor.py +4 -4
- v0/relationalai/early_access/dsl/snow/api.py +2 -1
- v0/relationalai/errors.py +18 -0
- v0/relationalai/experimental/solvers.py +7 -7
- v0/relationalai/semantics/devtools/benchmark_lqp.py +4 -5
- v0/relationalai/semantics/devtools/extract_lqp.py +1 -1
- v0/relationalai/semantics/internal/snowflake.py +1 -1
- v0/relationalai/semantics/lqp/executor.py +7 -12
- v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
- v0/relationalai/semantics/metamodel/util.py +6 -5
- v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +335 -84
- v0/relationalai/semantics/rel/executor.py +14 -11
- v0/relationalai/semantics/sql/executor/snowflake.py +9 -5
- v0/relationalai/semantics/tests/test_snapshot_abstract.py +1 -1
- v0/relationalai/tools/cli.py +26 -30
- v0/relationalai/tools/cli_helpers.py +10 -2
- v0/relationalai/util/otel_configuration.py +2 -1
- v0/relationalai/util/otel_handler.py +1 -1
- {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/WHEEL +0 -0
- {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/entry_points.txt +0 -0
- {relationalai-1.0.0a2.dist-info → relationalai-1.0.0a4.dist-info}/top_level.txt +0 -0
- /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
|
|
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 v0.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
|
|
@@ -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
|
-
|
|
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
|
+
)
|