relationalai 0.12.3__py3-none-any.whl → 0.12.6__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 +4 -0
- relationalai/clients/snowflake.py +129 -28
- relationalai/clients/use_index_poller.py +3 -0
- relationalai/{semantics/reasoners/graph → experimental}/paths/README.md +2 -2
- relationalai/experimental/paths/__init__.py +14 -309
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/basic_example.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/paths_benchmark.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/paths_example.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/pattern_to_automaton.py +1 -1
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/one_sided_ball_repetition.py +1 -1
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/single.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/two_sided_balls_repetition.py +1 -1
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/two_sided_balls_upto.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp-old.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp-tuple.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_max_length.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_walks_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_walks_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_repetition_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_repetition_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_upto_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_upto_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_paths.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_walks.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_walks_undirected.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_repetition_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_repetition_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_upto_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_upto_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_usp_nsp_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_usp_nsp_single.py +2 -2
- relationalai/semantics/__init__.py +4 -0
- relationalai/semantics/internal/annotations.py +1 -0
- relationalai/semantics/internal/internal.py +2 -0
- relationalai/semantics/internal/snowflake.py +2 -3
- relationalai/semantics/lqp/builtins.py +1 -0
- relationalai/semantics/lqp/executor.py +15 -10
- relationalai/semantics/lqp/model2lqp.py +96 -4
- relationalai/semantics/lqp/primitives.py +3 -0
- relationalai/semantics/lqp/rewrite/extract_common.py +26 -6
- relationalai/semantics/metamodel/builtins.py +50 -1
- relationalai/semantics/metamodel/dependency.py +14 -11
- relationalai/semantics/metamodel/rewrite/flatten.py +1 -13
- relationalai/semantics/metamodel/typer/typer.py +3 -0
- relationalai/semantics/reasoners/__init__.py +4 -0
- relationalai/semantics/reasoners/experimental/__init__.py +7 -0
- relationalai/semantics/reasoners/graph/core.py +1226 -148
- relationalai/semantics/rel/builtins.py +3 -1
- relationalai/semantics/rel/executor.py +13 -6
- relationalai/semantics/rel/rel_utils.py +5 -0
- relationalai/semantics/sql/compiler.py +6 -0
- relationalai/semantics/sql/executor/snowflake.py +2 -2
- {relationalai-0.12.3.dist-info → relationalai-0.12.6.dist-info}/METADATA +1 -1
- {relationalai-0.12.3.dist-info → relationalai-0.12.6.dist-info}/RECORD +92 -108
- {relationalai-0.12.3.dist-info → relationalai-0.12.6.dist-info}/WHEEL +1 -1
- relationalai/early_access/paths/__init__.py +0 -22
- relationalai/early_access/paths/api/__init__.py +0 -12
- relationalai/early_access/paths/benchmarks/__init__.py +0 -13
- relationalai/early_access/paths/graph/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/find_paths/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/one_sided_ball_repetition/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/one_sided_ball_upto/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/single/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/two_sided_balls_repetition/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/two_sided_balls_upto/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/usp/__init__.py +0 -12
- relationalai/early_access/paths/rpq/__init__.py +0 -13
- relationalai/early_access/paths/utilities/iterators/__init__.py +0 -12
- relationalai/experimental/paths/pathfinder.rel +0 -2560
- relationalai/semantics/reasoners/graph/paths/__init__.py +0 -16
- relationalai/semantics/reasoners/graph/paths/path_algorithms/__init__.py +0 -3
- relationalai/semantics/reasoners/graph/paths/utilities/__init__.py +0 -3
- /relationalai/{semantics/reasoners/graph → experimental}/paths/api.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/benchmarks/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/benchmarks/grid_graph.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/code_organization.md +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/Movies.ipynb +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/minimal_engine_warmup.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movie_example.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/actedin.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/directed.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/follows.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/movies.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/person.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/produced.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/ratings.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/wrote.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/find_paths_via_automaton.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/graph.py +0 -0
- /relationalai/{early_access → experimental}/paths/path_algorithms/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/find_paths.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/one_sided_ball_upto.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/product_graph.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/automaton.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/diagnostics.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/filter.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/glushkov.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/rpq.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/transition.py +0 -0
- /relationalai/{early_access → experimental}/paths/utilities/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/iterators.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/prefix_sum.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/utilities.py +0 -0
- {relationalai-0.12.3.dist-info → relationalai-0.12.6.dist-info}/entry_points.txt +0 -0
- {relationalai-0.12.3.dist-info → relationalai-0.12.6.dist-info}/licenses/LICENSE +0 -0
relationalai/__init__.py
CHANGED
|
@@ -83,6 +83,7 @@ FIELD_MAP = {
|
|
|
83
83
|
VALID_IMPORT_STATES = ["PENDING", "PROCESSING", "QUARANTINED", "LOADED"]
|
|
84
84
|
ENGINE_ERRORS = ["engine is suspended", "create/resume", "engine not found", "no engines found", "engine was deleted"]
|
|
85
85
|
ENGINE_NOT_READY_MSGS = ["engine is in pending", "engine is provisioning"]
|
|
86
|
+
DATABASE_ERRORS = ["database not found"]
|
|
86
87
|
PYREL_ROOT_DB = 'pyrel_root_db'
|
|
87
88
|
|
|
88
89
|
TERMINAL_TXN_STATES = ["COMPLETED", "ABORTED"]
|
|
@@ -281,6 +282,8 @@ def _sanitize_user_name(user: str) -> str:
|
|
|
281
282
|
def _is_engine_issue(response_message: str) -> bool:
|
|
282
283
|
return any(kw in response_message.lower() for kw in ENGINE_ERRORS + ENGINE_NOT_READY_MSGS)
|
|
283
284
|
|
|
285
|
+
def _is_database_issue(response_message: str) -> bool:
|
|
286
|
+
return any(kw in response_message.lower() for kw in DATABASE_ERRORS)
|
|
284
287
|
|
|
285
288
|
|
|
286
289
|
#--------------------------------------------------
|
|
@@ -298,6 +301,7 @@ class Resources(ResourcesBase):
|
|
|
298
301
|
dry_run: bool = False,
|
|
299
302
|
reset_session: bool = False,
|
|
300
303
|
generation: Generation | None = None,
|
|
304
|
+
language: str = "rel",
|
|
301
305
|
):
|
|
302
306
|
super().__init__(profile, config=config)
|
|
303
307
|
self._token_handler: TokenHandler | None = None
|
|
@@ -315,6 +319,8 @@ class Resources(ResourcesBase):
|
|
|
315
319
|
# self.sources contains fully qualified Snowflake table/view names
|
|
316
320
|
self.sources: set[str] = set()
|
|
317
321
|
self._sproc_models = None
|
|
322
|
+
self.database = ""
|
|
323
|
+
self.language = language
|
|
318
324
|
atexit.register(self.cancel_pending_transactions)
|
|
319
325
|
|
|
320
326
|
@property
|
|
@@ -452,6 +458,7 @@ class Resources(ResourcesBase):
|
|
|
452
458
|
rai_app = self.config.get("rai_app_name", "")
|
|
453
459
|
current_role = self.config.get("role")
|
|
454
460
|
engine = self.get_default_engine_name()
|
|
461
|
+
engine_size = self.config.get_default_engine_size()
|
|
455
462
|
assert isinstance(rai_app, str), f"rai_app_name must be a string, not {type(rai_app)}"
|
|
456
463
|
assert isinstance(engine, str), f"engine must be a string, not {type(engine)}"
|
|
457
464
|
print("\n")
|
|
@@ -460,9 +467,15 @@ class Resources(ResourcesBase):
|
|
|
460
467
|
if re.search(f"database '{rai_app}' does not exist or not authorized.".lower(), orig_message):
|
|
461
468
|
exception = SnowflakeAppMissingException(rai_app, current_role)
|
|
462
469
|
raise exception from None
|
|
463
|
-
if
|
|
470
|
+
if _is_engine_issue(orig_message) or _is_database_issue(orig_message):
|
|
464
471
|
try:
|
|
465
|
-
self.
|
|
472
|
+
self._poll_use_index(
|
|
473
|
+
app_name=self.get_app_name(),
|
|
474
|
+
sources=self.sources,
|
|
475
|
+
model=self.database,
|
|
476
|
+
engine_name=engine,
|
|
477
|
+
engine_size=engine_size
|
|
478
|
+
)
|
|
466
479
|
return self._exec(code, params, raw=raw, help=help)
|
|
467
480
|
except EngineNameValidationException as e:
|
|
468
481
|
raise EngineNameValidationException(engine) from e
|
|
@@ -767,7 +780,7 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
767
780
|
keep_database = not force and self.config.get("reuse_model", True)
|
|
768
781
|
with debugging.span("release_index", name=name, keep_database=keep_database, language=language):
|
|
769
782
|
#TODO add headers to release_index
|
|
770
|
-
response = self._exec(f"call {APP_NAME}.api.release_index('{name}', OBJECT_CONSTRUCT('keep_database', {keep_database}, 'language', '{language}'));")
|
|
783
|
+
response = self._exec(f"call {APP_NAME}.api.release_index('{name}', OBJECT_CONSTRUCT('keep_database', {keep_database}, 'language', '{language}', 'user_agent', '{get_pyrel_version(self.generation)}'));")
|
|
771
784
|
if response:
|
|
772
785
|
result = next(iter(response))
|
|
773
786
|
obj = json.loads(result["RELEASE_INDEX"])
|
|
@@ -788,14 +801,13 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
788
801
|
headers = debugging.gen_current_propagation_headers()
|
|
789
802
|
self._exec(f"call {APP_NAME}.api.clone_database('{target_name}', '{source_name}', {nowait_durable}, {headers});")
|
|
790
803
|
|
|
791
|
-
def
|
|
804
|
+
def _poll_use_index(
|
|
792
805
|
self,
|
|
793
806
|
app_name: str,
|
|
794
807
|
sources: Iterable[str],
|
|
795
808
|
model: str,
|
|
796
809
|
engine_name: str,
|
|
797
810
|
engine_size: str | None = None,
|
|
798
|
-
language: str = "rel",
|
|
799
811
|
program_span_id: str | None = None,
|
|
800
812
|
headers: Dict | None = None,
|
|
801
813
|
):
|
|
@@ -806,12 +818,42 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
806
818
|
model,
|
|
807
819
|
engine_name,
|
|
808
820
|
engine_size,
|
|
809
|
-
language,
|
|
821
|
+
self.language,
|
|
810
822
|
program_span_id,
|
|
811
823
|
headers,
|
|
812
824
|
self.generation
|
|
813
825
|
).poll()
|
|
814
826
|
|
|
827
|
+
def maybe_poll_use_index(
|
|
828
|
+
self,
|
|
829
|
+
app_name: str,
|
|
830
|
+
sources: Iterable[str],
|
|
831
|
+
model: str,
|
|
832
|
+
engine_name: str,
|
|
833
|
+
engine_size: str | None = None,
|
|
834
|
+
program_span_id: str | None = None,
|
|
835
|
+
headers: Dict | None = None,
|
|
836
|
+
):
|
|
837
|
+
"""Only call poll() if there are sources to process and cache is not valid."""
|
|
838
|
+
sources_list = list(sources)
|
|
839
|
+
self.database = model
|
|
840
|
+
if sources_list:
|
|
841
|
+
poller = UseIndexPoller(
|
|
842
|
+
self,
|
|
843
|
+
app_name,
|
|
844
|
+
sources_list,
|
|
845
|
+
model,
|
|
846
|
+
engine_name,
|
|
847
|
+
engine_size,
|
|
848
|
+
self.language,
|
|
849
|
+
program_span_id,
|
|
850
|
+
headers,
|
|
851
|
+
self.generation
|
|
852
|
+
)
|
|
853
|
+
# If cache is valid (data freshness has not expired), skip polling
|
|
854
|
+
if not poller.cache.is_valid():
|
|
855
|
+
return poller.poll()
|
|
856
|
+
|
|
815
857
|
#--------------------------------------------------
|
|
816
858
|
# Models
|
|
817
859
|
#--------------------------------------------------
|
|
@@ -1868,9 +1910,19 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1868
1910
|
)
|
|
1869
1911
|
except Exception as e:
|
|
1870
1912
|
err_message = str(e).lower()
|
|
1871
|
-
if _is_engine_issue(err_message):
|
|
1872
|
-
self.
|
|
1873
|
-
self.
|
|
1913
|
+
if _is_engine_issue(err_message) or _is_database_issue(err_message):
|
|
1914
|
+
engine_name = engine or self.get_default_engine_name()
|
|
1915
|
+
engine_size = self.config.get_default_engine_size()
|
|
1916
|
+
self._poll_use_index(
|
|
1917
|
+
app_name=self.get_app_name(),
|
|
1918
|
+
sources=self.sources,
|
|
1919
|
+
model=database,
|
|
1920
|
+
engine_name=engine_name,
|
|
1921
|
+
engine_size=engine_size,
|
|
1922
|
+
headers=headers,
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
return self._exec_async_v2(
|
|
1874
1926
|
database, engine, raw_code_b64, inputs, readonly, nowait_durable,
|
|
1875
1927
|
headers=headers, bypass_index=bypass_index, language='lqp',
|
|
1876
1928
|
query_timeout_mins=query_timeout_mins,
|
|
@@ -1908,8 +1960,17 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1908
1960
|
)
|
|
1909
1961
|
except Exception as e:
|
|
1910
1962
|
err_message = str(e).lower()
|
|
1911
|
-
if _is_engine_issue(err_message):
|
|
1912
|
-
self.
|
|
1963
|
+
if _is_engine_issue(err_message) or _is_database_issue(err_message):
|
|
1964
|
+
engine_name = engine or self.get_default_engine_name()
|
|
1965
|
+
engine_size = self.config.get_default_engine_size()
|
|
1966
|
+
self._poll_use_index(
|
|
1967
|
+
app_name=self.get_app_name(),
|
|
1968
|
+
sources=self.sources,
|
|
1969
|
+
model=database,
|
|
1970
|
+
engine_name=engine_name,
|
|
1971
|
+
engine_size=engine_size,
|
|
1972
|
+
headers=headers,
|
|
1973
|
+
)
|
|
1913
1974
|
return self._exec_async_v2(
|
|
1914
1975
|
database,
|
|
1915
1976
|
engine,
|
|
@@ -2972,13 +3033,12 @@ class SnowflakeClient(Client):
|
|
|
2972
3033
|
|
|
2973
3034
|
query_attrs_dict = json.loads(headers.get("X-Query-Attributes", "{}")) if headers else {}
|
|
2974
3035
|
with debugging.span("poll_use_index", sources=self.resources.sources, model=model, engine=engine_name, **query_attrs_dict):
|
|
2975
|
-
self.
|
|
3036
|
+
self.maybe_poll_use_index(
|
|
2976
3037
|
app_name=app_name,
|
|
2977
3038
|
sources=self.resources.sources,
|
|
2978
3039
|
model=model,
|
|
2979
3040
|
engine_name=engine_name,
|
|
2980
3041
|
engine_size=engine_size,
|
|
2981
|
-
language="rel",
|
|
2982
3042
|
program_span_id=program_span_id,
|
|
2983
3043
|
headers=headers
|
|
2984
3044
|
)
|
|
@@ -2989,29 +3049,24 @@ class SnowflakeClient(Client):
|
|
|
2989
3049
|
if isolated and not self.keep_model:
|
|
2990
3050
|
atexit.register(self.delete_database)
|
|
2991
3051
|
|
|
2992
|
-
|
|
2993
|
-
# if data is ready, break the loop
|
|
2994
|
-
# if data is not ready, print the status of the tables or engines
|
|
2995
|
-
# if data is not ready and there are errors, collect the errors and raise exceptions
|
|
2996
|
-
def poll_use_index(
|
|
3052
|
+
def maybe_poll_use_index(
|
|
2997
3053
|
self,
|
|
2998
3054
|
app_name: str,
|
|
2999
3055
|
sources: Iterable[str],
|
|
3000
3056
|
model: str,
|
|
3001
3057
|
engine_name: str,
|
|
3002
3058
|
engine_size: str | None = None,
|
|
3003
|
-
language: str = "rel",
|
|
3004
3059
|
program_span_id: str | None = None,
|
|
3005
3060
|
headers: Dict | None = None,
|
|
3006
3061
|
):
|
|
3062
|
+
"""Only call _poll_use_index if there are sources to process."""
|
|
3007
3063
|
assert isinstance(self.resources, Resources)
|
|
3008
|
-
return self.resources.
|
|
3064
|
+
return self.resources.maybe_poll_use_index(
|
|
3009
3065
|
app_name=app_name,
|
|
3010
3066
|
sources=sources,
|
|
3011
3067
|
model=model,
|
|
3012
3068
|
engine_name=engine_name,
|
|
3013
3069
|
engine_size=engine_size,
|
|
3014
|
-
language=language,
|
|
3015
3070
|
program_span_id=program_span_id,
|
|
3016
3071
|
headers=headers
|
|
3017
3072
|
)
|
|
@@ -3136,6 +3191,7 @@ class DirectAccessResources(Resources):
|
|
|
3136
3191
|
dry_run: bool = False,
|
|
3137
3192
|
reset_session: bool = False,
|
|
3138
3193
|
generation: Optional[Generation] = None,
|
|
3194
|
+
language: str = "rel",
|
|
3139
3195
|
):
|
|
3140
3196
|
super().__init__(
|
|
3141
3197
|
generation=generation,
|
|
@@ -3144,11 +3200,13 @@ class DirectAccessResources(Resources):
|
|
|
3144
3200
|
connection=connection,
|
|
3145
3201
|
reset_session=reset_session,
|
|
3146
3202
|
dry_run=dry_run,
|
|
3203
|
+
language=language,
|
|
3147
3204
|
)
|
|
3148
3205
|
self._endpoint_info = ConfigStore(ENDPOINT_FILE)
|
|
3149
3206
|
self._service_endpoint = ""
|
|
3150
3207
|
self._direct_access_client = None
|
|
3151
3208
|
self.generation = generation
|
|
3209
|
+
self.database = ""
|
|
3152
3210
|
|
|
3153
3211
|
@property
|
|
3154
3212
|
def service_endpoint(self) -> str:
|
|
@@ -3226,9 +3284,18 @@ class DirectAccessResources(Resources):
|
|
|
3226
3284
|
|
|
3227
3285
|
# fix engine on engine error and retry
|
|
3228
3286
|
# Skip auto-retry if skip_auto_create is True to avoid recursion
|
|
3229
|
-
if _is_engine_issue(message) and not skip_auto_create:
|
|
3230
|
-
|
|
3231
|
-
self.
|
|
3287
|
+
if (_is_engine_issue(message) and not skip_auto_create) or _is_database_issue(message):
|
|
3288
|
+
engine_name = payload.get("caller_engine_name", "") if payload else ""
|
|
3289
|
+
engine_name = engine_name or self.get_default_engine_name()
|
|
3290
|
+
engine_size = self.config.get_default_engine_size()
|
|
3291
|
+
self._poll_use_index(
|
|
3292
|
+
app_name=self.get_app_name(),
|
|
3293
|
+
sources=self.sources,
|
|
3294
|
+
model=self.database,
|
|
3295
|
+
engine_name=engine_name,
|
|
3296
|
+
engine_size=engine_size,
|
|
3297
|
+
headers=headers,
|
|
3298
|
+
)
|
|
3232
3299
|
response = _send_request()
|
|
3233
3300
|
except requests.exceptions.ConnectionError as e:
|
|
3234
3301
|
if "NameResolutionError" in str(e):
|
|
@@ -3356,14 +3423,13 @@ class DirectAccessResources(Resources):
|
|
|
3356
3423
|
|
|
3357
3424
|
return response.json()
|
|
3358
3425
|
|
|
3359
|
-
def
|
|
3426
|
+
def _poll_use_index(
|
|
3360
3427
|
self,
|
|
3361
3428
|
app_name: str,
|
|
3362
3429
|
sources: Iterable[str],
|
|
3363
3430
|
model: str,
|
|
3364
3431
|
engine_name: str,
|
|
3365
3432
|
engine_size: str | None = None,
|
|
3366
|
-
language: str = "rel",
|
|
3367
3433
|
program_span_id: str | None = None,
|
|
3368
3434
|
headers: Dict | None = None,
|
|
3369
3435
|
):
|
|
@@ -3374,12 +3440,42 @@ class DirectAccessResources(Resources):
|
|
|
3374
3440
|
model=model,
|
|
3375
3441
|
engine_name=engine_name,
|
|
3376
3442
|
engine_size=engine_size,
|
|
3377
|
-
language=language,
|
|
3443
|
+
language=self.language,
|
|
3378
3444
|
program_span_id=program_span_id,
|
|
3379
3445
|
headers=headers,
|
|
3380
3446
|
generation=self.generation,
|
|
3381
3447
|
).poll()
|
|
3382
3448
|
|
|
3449
|
+
def maybe_poll_use_index(
|
|
3450
|
+
self,
|
|
3451
|
+
app_name: str,
|
|
3452
|
+
sources: Iterable[str],
|
|
3453
|
+
model: str,
|
|
3454
|
+
engine_name: str,
|
|
3455
|
+
engine_size: str | None = None,
|
|
3456
|
+
program_span_id: str | None = None,
|
|
3457
|
+
headers: Dict | None = None,
|
|
3458
|
+
):
|
|
3459
|
+
"""Only call poll() if there are sources to process and cache is not valid."""
|
|
3460
|
+
sources_list = list(sources)
|
|
3461
|
+
self.database = model
|
|
3462
|
+
if sources_list:
|
|
3463
|
+
poller = DirectUseIndexPoller(
|
|
3464
|
+
self,
|
|
3465
|
+
app_name=app_name,
|
|
3466
|
+
sources=sources_list,
|
|
3467
|
+
model=model,
|
|
3468
|
+
engine_name=engine_name,
|
|
3469
|
+
engine_size=engine_size,
|
|
3470
|
+
language=self.language,
|
|
3471
|
+
program_span_id=program_span_id,
|
|
3472
|
+
headers=headers,
|
|
3473
|
+
generation=self.generation,
|
|
3474
|
+
)
|
|
3475
|
+
# If cache is valid (data freshness has not expired), skip polling
|
|
3476
|
+
if not poller.cache.is_valid():
|
|
3477
|
+
return poller.poll()
|
|
3478
|
+
|
|
3383
3479
|
def _check_exec_async_status(self, txn_id: str, headers: Dict[str, str] | None = None) -> bool:
|
|
3384
3480
|
"""Check whether the given transaction has completed."""
|
|
3385
3481
|
|
|
@@ -3522,7 +3618,12 @@ class DirectAccessResources(Resources):
|
|
|
3522
3618
|
with debugging.span("release_index", name=name, keep_database=keep_database, language=language):
|
|
3523
3619
|
response = self.request(
|
|
3524
3620
|
"release_index",
|
|
3525
|
-
payload={
|
|
3621
|
+
payload={
|
|
3622
|
+
"model_name": name,
|
|
3623
|
+
"keep_database": keep_database,
|
|
3624
|
+
"language": language,
|
|
3625
|
+
"user_agent": get_pyrel_version(self.generation),
|
|
3626
|
+
},
|
|
3526
3627
|
headers=prop_hdrs,
|
|
3527
3628
|
)
|
|
3528
3629
|
if (
|
|
@@ -12,8 +12,8 @@ The following is a minimal working example:
|
|
|
12
12
|
from relationalai.semantics import Model, Integer, select, String, where, define
|
|
13
13
|
from relationalai.semantics.std import strings
|
|
14
14
|
|
|
15
|
-
from relationalai.
|
|
16
|
-
from relationalai.
|
|
15
|
+
from relationalai.experimental.paths.graph import Graph
|
|
16
|
+
from relationalai.experimental.paths.api import node, edge, path, star, match
|
|
17
17
|
|
|
18
18
|
model = Model("my_paths")
|
|
19
19
|
|
|
@@ -1,309 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self.max = 1
|
|
16
|
-
|
|
17
|
-
def __getitem__(self, key):
|
|
18
|
-
if isinstance(key, slice):
|
|
19
|
-
self.min = key.start
|
|
20
|
-
self.max = key.stop
|
|
21
|
-
else:
|
|
22
|
-
raise ValueError("VarLength only supports slicing")
|
|
23
|
-
return self
|
|
24
|
-
|
|
25
|
-
class PathSelection(Enum):
|
|
26
|
-
ALL = "all" # return all paths between source and target
|
|
27
|
-
SINGLE = "single" # return a single path between source and target
|
|
28
|
-
LIMIT = "limit" # return a specified number paths between source and target, as given by parameter k.
|
|
29
|
-
RANDOM = "random" # return a single randomly-chosen path between source and target, seeded with parameter seed.
|
|
30
|
-
UNIFORM_RANDOM = "uniform_random" # return a single uniform randomly-chosen path between source and target, seeded with parameter seed.
|
|
31
|
-
|
|
32
|
-
class PathGroup(Enum):
|
|
33
|
-
ANY = "any"
|
|
34
|
-
FOREACH = "for_each"
|
|
35
|
-
|
|
36
|
-
class Strategy(Enum):
|
|
37
|
-
FROM_SOURCE = "from_source"
|
|
38
|
-
TWO_SIDED = "two_sided"
|
|
39
|
-
|
|
40
|
-
class PathQuery:
|
|
41
|
-
|
|
42
|
-
def __init__(
|
|
43
|
-
self,
|
|
44
|
-
model: Graph,
|
|
45
|
-
segments: List[Union[Instance,VarLength]],
|
|
46
|
-
backwards_edge: str,
|
|
47
|
-
group: PathGroup = PathGroup.FOREACH,
|
|
48
|
-
selection: PathSelection = PathSelection.ALL,
|
|
49
|
-
strategy: Strategy = Strategy.FROM_SOURCE,
|
|
50
|
-
):
|
|
51
|
-
# create types and instance
|
|
52
|
-
self._path_id = next_id()
|
|
53
|
-
self._conn_relation_name = f"conn_{self._path_id}"
|
|
54
|
-
self._model = model
|
|
55
|
-
|
|
56
|
-
# create types
|
|
57
|
-
self._Path = dsl.Type(model, f"Path{self._path_id}", omit_intrinsic_type_in_hash=True)
|
|
58
|
-
self._PathNode = dsl.Type(model, f"PathNode{self._path_id}", omit_intrinsic_type_in_hash=True)
|
|
59
|
-
self._PathEdge = dsl.Type(model, f"PathEdge{self._path_id}", omit_intrinsic_type_in_hash=True)
|
|
60
|
-
|
|
61
|
-
# declare multivalued attributes
|
|
62
|
-
self._Path.nodes.has_many()
|
|
63
|
-
self._Path.edges.has_many()
|
|
64
|
-
|
|
65
|
-
# mark these types and their attributes as @no_inline
|
|
66
|
-
self._add_noinline(self._Path, [])
|
|
67
|
-
self._add_noinline(self._PathNode, ["path", "index", "node_id"])
|
|
68
|
-
self._add_noinline(self._PathEdge, ["path", "index", "label"])
|
|
69
|
-
|
|
70
|
-
# create instance
|
|
71
|
-
self._instance = self._Path()
|
|
72
|
-
self._match_called = False
|
|
73
|
-
|
|
74
|
-
# store path query attributes
|
|
75
|
-
self._segments = segments
|
|
76
|
-
self._backwards_edge = backwards_edge
|
|
77
|
-
self._group = group
|
|
78
|
-
self._selection = selection
|
|
79
|
-
self._strategy = strategy
|
|
80
|
-
|
|
81
|
-
def _add_noinline(self, typ, props):
|
|
82
|
-
typ._type.parents.append(Builtins.NoInlineAnnotation)
|
|
83
|
-
for prop in props:
|
|
84
|
-
getattr(typ, prop)._prop.parents.append(Builtins.NoInlineAnnotation)
|
|
85
|
-
|
|
86
|
-
def _match(self):
|
|
87
|
-
# avoid emitting the Rel multiple times, which throws off Pathfinder
|
|
88
|
-
if not self._match_called:
|
|
89
|
-
self._match_called = True
|
|
90
|
-
|
|
91
|
-
begin_var, end_var = self._match_inner()
|
|
92
|
-
|
|
93
|
-
self._begin_var = begin_var
|
|
94
|
-
self._end_var = end_var
|
|
95
|
-
|
|
96
|
-
return self._conn_relation_name
|
|
97
|
-
|
|
98
|
-
def _match_inner(self):
|
|
99
|
-
begin_var = None
|
|
100
|
-
cur_end = None
|
|
101
|
-
|
|
102
|
-
for idx, segment in enumerate(self._segments):
|
|
103
|
-
if isinstance(segment, VarLength):
|
|
104
|
-
var_length_rel_name = self._var_length(idx, segment)
|
|
105
|
-
seg_begin, seg_end = create_vars(2)
|
|
106
|
-
getattr(rel, var_length_rel_name)(seg_begin, seg_end)
|
|
107
|
-
|
|
108
|
-
if begin_var is None:
|
|
109
|
-
begin_var = seg_begin
|
|
110
|
-
cur_end = seg_end
|
|
111
|
-
else:
|
|
112
|
-
getattr(seg_begin, self._backwards_edge) == cur_end
|
|
113
|
-
cur_end = seg_end
|
|
114
|
-
else:
|
|
115
|
-
next_var = segment
|
|
116
|
-
if begin_var is None:
|
|
117
|
-
begin_var = next_var
|
|
118
|
-
cur_end = begin_var
|
|
119
|
-
else:
|
|
120
|
-
getattr(next_var, self._backwards_edge) == cur_end
|
|
121
|
-
cur_end = next_var
|
|
122
|
-
|
|
123
|
-
conn_relation = getattr(rel, self._conn_relation_name)
|
|
124
|
-
conn_relation._rel.parents.append(Builtins.NoInlineAnnotation)
|
|
125
|
-
conn_relation.add(begin_var, cur_end)
|
|
126
|
-
|
|
127
|
-
return begin_var, cur_end
|
|
128
|
-
|
|
129
|
-
def _var_length(self, segment_idx: int, segment: VarLength):
|
|
130
|
-
# relations for each length
|
|
131
|
-
prefix = f"conn_{self._path_id}_segment_{segment_idx}"
|
|
132
|
-
|
|
133
|
-
lines = []
|
|
134
|
-
|
|
135
|
-
for length in range(1, segment.max+1):
|
|
136
|
-
length_rel_name = f"{prefix}_length_{length}"
|
|
137
|
-
|
|
138
|
-
if length == 1:
|
|
139
|
-
lines.extend([
|
|
140
|
-
f"def {length_rel_name}(a, a1):",
|
|
141
|
-
f" {segment.type._type.name}(a) and",
|
|
142
|
-
" a = a1"
|
|
143
|
-
])
|
|
144
|
-
# would just project `(a, a)`, but that gives a
|
|
145
|
-
# bunch of Rel warnings`
|
|
146
|
-
|
|
147
|
-
else:
|
|
148
|
-
prev_length_relation_name = f"{prefix}_length_{length-1}"
|
|
149
|
-
lines.extend([
|
|
150
|
-
f"def {length_rel_name}(a, c):",
|
|
151
|
-
" exists((b) |",
|
|
152
|
-
f" {prev_length_relation_name}(a, b) and",
|
|
153
|
-
f" {segment.type._type.name}(c) and",
|
|
154
|
-
f" {self._backwards_edge}(c, b)",
|
|
155
|
-
" )"
|
|
156
|
-
])
|
|
157
|
-
|
|
158
|
-
# union them all together in overall segment relation
|
|
159
|
-
lines.extend([
|
|
160
|
-
f"def {prefix}(start, finish):",
|
|
161
|
-
" " + " or\n ".join([
|
|
162
|
-
f"{prefix}_length_{length}(start, finish)"
|
|
163
|
-
for length in range(1, segment.max+1)
|
|
164
|
-
]),
|
|
165
|
-
])
|
|
166
|
-
|
|
167
|
-
self._model.install_raw(
|
|
168
|
-
'\n'.join(lines),
|
|
169
|
-
name=f"path_{self._path_id}_segment_{segment_idx}",
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
return prefix
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class PathInstance(Instance):
|
|
176
|
-
|
|
177
|
-
def __init__(
|
|
178
|
-
self,
|
|
179
|
-
path_query: PathQuery,
|
|
180
|
-
):
|
|
181
|
-
self._path_query = path_query
|
|
182
|
-
|
|
183
|
-
self._already_called = False
|
|
184
|
-
super().__init__(
|
|
185
|
-
self._path_query._model,
|
|
186
|
-
ActionType.Get, [self._path_query._instance],
|
|
187
|
-
named={},
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
# implements producer API
|
|
191
|
-
def _to_var(self):
|
|
192
|
-
self._invoke_pathfinder()
|
|
193
|
-
return super()._to_var()
|
|
194
|
-
|
|
195
|
-
def _invoke_pathfinder(self):
|
|
196
|
-
if self._already_called:
|
|
197
|
-
return
|
|
198
|
-
self._already_called = True
|
|
199
|
-
|
|
200
|
-
# ensure that the pathfinder Rel library is installed
|
|
201
|
-
_install_pathfinder(self._path_query._model)
|
|
202
|
-
|
|
203
|
-
source_rel = f"source_{self._path_query._path_id}"
|
|
204
|
-
target_rel = f"target_{self._path_query._path_id}"
|
|
205
|
-
getattr(rel, source_rel).add(self._path_query._begin_var)
|
|
206
|
-
getattr(rel, target_rel).add(self._path_query._end_var)
|
|
207
|
-
|
|
208
|
-
shortest_paths_rel = f"shortest_paths_{self._path_query._path_id}"
|
|
209
|
-
path_edge_rel = f"path_edge_{self._path_query._path_id}"
|
|
210
|
-
path_node_rel = f"path_node_{self._path_query._path_id}"
|
|
211
|
-
|
|
212
|
-
invocation = f"""
|
|
213
|
-
@no_inline
|
|
214
|
-
def {shortest_paths_rel} {{
|
|
215
|
-
::pathfinder::shortest_paths[
|
|
216
|
-
:{self._path_query._group.value},
|
|
217
|
-
:{self._path_query._selection.value},
|
|
218
|
-
:{self._path_query._strategy.value},
|
|
219
|
-
{self._path_query._conn_relation_name},
|
|
220
|
-
{source_rel},
|
|
221
|
-
{target_rel}
|
|
222
|
-
]
|
|
223
|
-
}}
|
|
224
|
-
|
|
225
|
-
@no_inline
|
|
226
|
-
def {path_node_rel}(path, index, id):
|
|
227
|
-
{shortest_paths_rel}(path, :node, index, id)
|
|
228
|
-
|
|
229
|
-
@no_inline
|
|
230
|
-
def {path_edge_rel}(path, index, label):
|
|
231
|
-
{shortest_paths_rel}(path, :edge_label, index, label)
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
self._path_query._model.install_raw(invocation)
|
|
235
|
-
|
|
236
|
-
# select out nodes
|
|
237
|
-
with self._path_query._model.rule():
|
|
238
|
-
path_id, index, node_label = create_vars(3)
|
|
239
|
-
getattr(rel, path_node_rel)(path_id, index, node_label)
|
|
240
|
-
path = self._path_query._Path.add(id=path_id)
|
|
241
|
-
# TODO: avoid perf hit of going in both directions (path<->node)
|
|
242
|
-
node = self._path_query._PathNode.add(path=path, index=index, value=node_label)
|
|
243
|
-
path.nodes.add(node)
|
|
244
|
-
|
|
245
|
-
# select out edges
|
|
246
|
-
with self._path_query._model.rule():
|
|
247
|
-
path_id, index, label = create_vars(3)
|
|
248
|
-
getattr(rel, path_edge_rel)(path_id, index, label)
|
|
249
|
-
path = self._path_query._Path.add(id=path_id)
|
|
250
|
-
# TODO: avoid perf hit of going in both directions (path<->edge)
|
|
251
|
-
edge = self._path_query._PathEdge.add(path=path, index=index, label=label)
|
|
252
|
-
path.edges.add(edge)
|
|
253
|
-
|
|
254
|
-
# TODO: automatically infer this from the path query itself
|
|
255
|
-
def enable_on(edge: Property, filter_attrs: List[Tuple[Type, Set[str]]]): # noqa: F821
|
|
256
|
-
edge._prop.parents.append(Builtins.PQEdgeAnnotation)
|
|
257
|
-
edge._prop.parents.append(Builtins.NoInlineAnnotation)
|
|
258
|
-
for type, attrs in filter_attrs:
|
|
259
|
-
type._type.parents.append(Builtins.PQFilterAnnotation)
|
|
260
|
-
type._type.parents.append(Builtins.NoInlineAnnotation)
|
|
261
|
-
for attr_name in attrs:
|
|
262
|
-
prop = getattr(type, attr_name)
|
|
263
|
-
prop._prop.parents.append(Builtins.PQFilterAnnotation)
|
|
264
|
-
prop._prop.parents.append(Builtins.NoInlineAnnotation)
|
|
265
|
-
|
|
266
|
-
# main entry point
|
|
267
|
-
def path(
|
|
268
|
-
segments: List[Union[Instance,VarLength]],
|
|
269
|
-
backwards_edge: str,
|
|
270
|
-
group: PathGroup = PathGroup.FOREACH,
|
|
271
|
-
selection: PathSelection = PathSelection.ALL,
|
|
272
|
-
strategy: Strategy = Strategy.FROM_SOURCE,
|
|
273
|
-
):
|
|
274
|
-
"""
|
|
275
|
-
Find a path consisting of the provided segments, linked by
|
|
276
|
-
the provided `backwards_edge` property.
|
|
277
|
-
"""
|
|
278
|
-
|
|
279
|
-
model = get_graph()
|
|
280
|
-
|
|
281
|
-
query = PathQuery(model, segments, backwards_edge, group, selection, strategy)
|
|
282
|
-
|
|
283
|
-
# declare filter relations
|
|
284
|
-
conn_relation_name = query._match()
|
|
285
|
-
|
|
286
|
-
# invoke filter
|
|
287
|
-
a, b = create_vars(2)
|
|
288
|
-
getattr(rel, conn_relation_name)(a, b)
|
|
289
|
-
|
|
290
|
-
# return a special Producer representing the path itself, which
|
|
291
|
-
# lazily invokes Pathfinder
|
|
292
|
-
return PathInstance(query)
|
|
293
|
-
|
|
294
|
-
def _get_pathfinder_source():
|
|
295
|
-
# check if the current dir is in a zip file
|
|
296
|
-
current_dir = os.path.dirname(__file__)
|
|
297
|
-
pathfinder_path = os.path.join(current_dir, "pathfinder.rel")
|
|
298
|
-
if os.path.exists(pathfinder_path):
|
|
299
|
-
return open(pathfinder_path).read()
|
|
300
|
-
# if we're in a zip file, read it within that
|
|
301
|
-
zip_split = current_dir.split(".zip")
|
|
302
|
-
zip_path = zip_split[0] + ".zip"
|
|
303
|
-
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
304
|
-
with zip_ref.open('relationalai/experimental/paths/pathfinder.rel') as file:
|
|
305
|
-
return file.read().decode("utf-8")
|
|
306
|
-
|
|
307
|
-
def _install_pathfinder(model: Graph):
|
|
308
|
-
source = _get_pathfinder_source()
|
|
309
|
-
model.install_raw(source, name="pathfinder", overwrite=True)
|
|
1
|
+
## pathfinder module
|
|
2
|
+
|
|
3
|
+
from .api import node, edge, path, optional, plus, star, union, match
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'node',
|
|
7
|
+
'edge',
|
|
8
|
+
'path',
|
|
9
|
+
'optional',
|
|
10
|
+
'plus',
|
|
11
|
+
'star',
|
|
12
|
+
'union',
|
|
13
|
+
'match',
|
|
14
|
+
]
|