relationalai 0.12.0__py3-none-any.whl → 0.12.2__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/clients/direct_access_client.py +5 -0
- relationalai/clients/snowflake.py +259 -91
- relationalai/clients/types.py +4 -1
- relationalai/clients/use_index_poller.py +96 -55
- relationalai/clients/util.py +9 -0
- relationalai/dsl.py +1 -2
- relationalai/environments/snowbook.py +10 -1
- relationalai/experimental/solvers.py +283 -79
- relationalai/semantics/internal/internal.py +24 -5
- relationalai/semantics/lqp/executor.py +22 -6
- relationalai/semantics/lqp/model2lqp.py +4 -2
- relationalai/semantics/metamodel/executor.py +2 -1
- relationalai/semantics/metamodel/rewrite/flatten.py +8 -7
- relationalai/semantics/reasoners/graph/core.py +1174 -226
- relationalai/semantics/rel/executor.py +30 -12
- relationalai/semantics/sql/executor/snowflake.py +1 -1
- relationalai/tools/cli.py +6 -2
- relationalai/tools/cli_controls.py +334 -352
- relationalai/tools/constants.py +1 -0
- relationalai/tools/query_utils.py +27 -0
- relationalai/util/otel_configuration.py +1 -1
- {relationalai-0.12.0.dist-info → relationalai-0.12.2.dist-info}/METADATA +1 -1
- {relationalai-0.12.0.dist-info → relationalai-0.12.2.dist-info}/RECORD +26 -25
- {relationalai-0.12.0.dist-info → relationalai-0.12.2.dist-info}/WHEEL +0 -0
- {relationalai-0.12.0.dist-info → relationalai-0.12.2.dist-info}/entry_points.txt +0 -0
- {relationalai-0.12.0.dist-info → relationalai-0.12.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -49,6 +49,11 @@ class DirectAccessClient:
|
|
|
49
49
|
"suspend_engine": Endpoint(method="POST", endpoint="/v1alpha1/engines/{engine_type}/{engine_name}/suspend"),
|
|
50
50
|
"resume_engine": Endpoint(method="POST", endpoint="/v1alpha1/engines/{engine_type}/{engine_name}/resume_async"),
|
|
51
51
|
"prepare_index": Endpoint(method="POST", endpoint="/v1alpha1/index/prepare"),
|
|
52
|
+
"get_job": Endpoint(method="GET", endpoint="/v1alpha1/jobs/{job_type}/{job_id}"),
|
|
53
|
+
"list_jobs": Endpoint(method="GET", endpoint="/v1alpha1/jobs"),
|
|
54
|
+
"get_job_events": Endpoint(method="GET", endpoint="/v1alpha1/jobs/{job_type}/{job_id}/events/{stream_name}"),
|
|
55
|
+
"create_job": Endpoint(method="POST", endpoint="/v1alpha1/jobs"),
|
|
56
|
+
"cancel_job": Endpoint(method="POST", endpoint="/v1alpha1/jobs/{job_type}/{job_id}/cancel"),
|
|
52
57
|
}
|
|
53
58
|
self.http_session = self._create_retry_session()
|
|
54
59
|
|
|
@@ -41,7 +41,7 @@ from ..clients.types import AvailableModel, EngineState, Import, ImportSource, I
|
|
|
41
41
|
from ..clients.config import Config, ConfigStore, ENDPOINT_FILE
|
|
42
42
|
from ..clients.client import Client, ExportParams, ProviderBase, ResourcesBase
|
|
43
43
|
from ..clients.direct_access_client import DirectAccessClient
|
|
44
|
-
from ..clients.util import IdentityParser, escape_for_f_string, get_pyrel_version, get_with_retries, poll_with_specified_overhead, safe_json_loads, sanitize_module_name, scrub_exception, wrap_with_request_id, ms_to_timestamp
|
|
44
|
+
from ..clients.util import IdentityParser, escape_for_f_string, get_pyrel_version, get_with_retries, poll_with_specified_overhead, safe_json_loads, sanitize_module_name, scrub_exception, wrap_with_request_id, ms_to_timestamp, normalize_datetime
|
|
45
45
|
from ..environments import runtime_env, HexEnvironment, SnowbookEnvironment
|
|
46
46
|
from .. import dsl, rel, metamodel as m
|
|
47
47
|
from ..errors import DuoSecurityFailed, EngineProvisioningFailed, EngineNameValidationException, EngineNotFoundException, EnginePending, EngineSizeMismatchWarning, EngineResumeFailed, Errors, InvalidAliasError, InvalidEngineSizeError, InvalidSourceTypeWarning, RAIAbortedTransactionError, RAIException, HexSessionException, SnowflakeAppMissingException, SnowflakeChangeTrackingNotEnabledException, SnowflakeDatabaseException, SnowflakeImportMissingException, SnowflakeInvalidSource, SnowflakeMissingConfigValuesException, SnowflakeProxyAPIDeprecationWarning, SnowflakeProxySourceError, SnowflakeRaiAppNotStarted, ModelNotFoundException, UnknownSourceWarning, ResponseStatusException, RowsDroppedFromTargetTableWarning, QueryTimeoutExceededException
|
|
@@ -761,13 +761,13 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
761
761
|
with debugging.span("create_model", name=name):
|
|
762
762
|
self._exec(f"call {APP_NAME}.api.create_database('{name}', false, {debugging.gen_current_propagation_headers()});")
|
|
763
763
|
|
|
764
|
-
def delete_graph(self, name:str, force=False):
|
|
764
|
+
def delete_graph(self, name:str, force=False, language:str="rel"):
|
|
765
765
|
prop_hdrs = debugging.gen_current_propagation_headers()
|
|
766
766
|
if self.config.get("use_graph_index", USE_GRAPH_INDEX):
|
|
767
767
|
keep_database = not force and self.config.get("reuse_model", True)
|
|
768
|
-
with debugging.span("release_index", name=name, keep_database=keep_database):
|
|
768
|
+
with debugging.span("release_index", name=name, keep_database=keep_database, language=language):
|
|
769
769
|
#TODO add headers to release_index
|
|
770
|
-
response = self._exec(f"call {APP_NAME}.api.release_index('{name}', OBJECT_CONSTRUCT('keep_database', {keep_database}));")
|
|
770
|
+
response = self._exec(f"call {APP_NAME}.api.release_index('{name}', OBJECT_CONSTRUCT('keep_database', {keep_database}, 'language', '{language}'));")
|
|
771
771
|
if response:
|
|
772
772
|
result = next(iter(response))
|
|
773
773
|
obj = json.loads(result["RELEASE_INDEX"])
|
|
@@ -795,6 +795,7 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
795
795
|
model: str,
|
|
796
796
|
engine_name: str,
|
|
797
797
|
engine_size: str | None = None,
|
|
798
|
+
language: str = "rel",
|
|
798
799
|
program_span_id: str | None = None,
|
|
799
800
|
headers: Dict | None = None,
|
|
800
801
|
):
|
|
@@ -805,6 +806,7 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
805
806
|
model,
|
|
806
807
|
engine_name,
|
|
807
808
|
engine_size,
|
|
809
|
+
language,
|
|
808
810
|
program_span_id,
|
|
809
811
|
headers,
|
|
810
812
|
self.generation
|
|
@@ -1867,7 +1869,7 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1867
1869
|
except Exception as e:
|
|
1868
1870
|
err_message = str(e).lower()
|
|
1869
1871
|
if _is_engine_issue(err_message):
|
|
1870
|
-
self.auto_create_engine(engine)
|
|
1872
|
+
self.auto_create_engine(engine, headers=headers)
|
|
1871
1873
|
self._exec_async_v2(
|
|
1872
1874
|
database, engine, raw_code_b64, inputs, readonly, nowait_durable,
|
|
1873
1875
|
headers=headers, bypass_index=bypass_index, language='lqp',
|
|
@@ -1907,7 +1909,7 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1907
1909
|
except Exception as e:
|
|
1908
1910
|
err_message = str(e).lower()
|
|
1909
1911
|
if _is_engine_issue(err_message):
|
|
1910
|
-
self.auto_create_engine(engine)
|
|
1912
|
+
self.auto_create_engine(engine, headers=headers)
|
|
1911
1913
|
return self._exec_async_v2(
|
|
1912
1914
|
database,
|
|
1913
1915
|
engine,
|
|
@@ -1970,9 +1972,9 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
1970
1972
|
if use_graph_index:
|
|
1971
1973
|
# we do not provide a default value for query_timeout_mins so that we can control the default on app level
|
|
1972
1974
|
if query_timeout_mins is not None:
|
|
1973
|
-
res = self._exec(f"call {APP_NAME}.api.exec_into_table(?, ?, ?, ?, ?, ?, ?, ?);", [database, engine, raw_code, output_table, readonly, nowait_durable, skip_invalid_data, query_timeout_mins])
|
|
1975
|
+
res = self._exec(f"call {APP_NAME}.api.exec_into_table(?, ?, ?, ?, ?, NULL, ?, {headers}, ?, ?);", [database, engine, raw_code, output_table, readonly, nowait_durable, skip_invalid_data, query_timeout_mins])
|
|
1974
1976
|
else:
|
|
1975
|
-
|
|
1977
|
+
res = self._exec(f"call {APP_NAME}.api.exec_into_table(?, ?, ?, ?, ?, NULL, ?, {headers}, ?);", [database, engine, raw_code, output_table, readonly, nowait_durable, skip_invalid_data])
|
|
1976
1978
|
txn_id = json.loads(res[0]["EXEC_INTO_TABLE"])["rai_transaction_id"]
|
|
1977
1979
|
rejected_rows = json.loads(res[0]["EXEC_INTO_TABLE"]).get("rejected_rows", [])
|
|
1978
1980
|
rejected_rows_count = json.loads(res[0]["EXEC_INTO_TABLE"]).get("rejected_rows_count", 0)
|
|
@@ -2047,9 +2049,10 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
2047
2049
|
app_name = self.get_app_name()
|
|
2048
2050
|
|
|
2049
2051
|
source_types = dict[str, SourceInfo]()
|
|
2050
|
-
partitioned_sources: dict[str, dict[str, list[str]]] = defaultdict(
|
|
2052
|
+
partitioned_sources: dict[str, dict[str, list[dict[str, str]]]] = defaultdict(
|
|
2051
2053
|
lambda: defaultdict(list)
|
|
2052
2054
|
)
|
|
2055
|
+
fqn_to_parts: dict[str, tuple[str, str, str]] = {}
|
|
2053
2056
|
|
|
2054
2057
|
for source in sources:
|
|
2055
2058
|
parser = IdentityParser(source, True)
|
|
@@ -2057,82 +2060,219 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
2057
2060
|
assert len(parsed) == 4, f"Invalid source: {source}"
|
|
2058
2061
|
db, schema, entity, identity = parsed
|
|
2059
2062
|
assert db and schema and entity and identity, f"Invalid source: {source}"
|
|
2060
|
-
source_types[identity] = cast(
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2063
|
+
source_types[identity] = cast(
|
|
2064
|
+
SourceInfo,
|
|
2065
|
+
{
|
|
2066
|
+
"type": None,
|
|
2067
|
+
"state": "",
|
|
2068
|
+
"columns_hash": None,
|
|
2069
|
+
"table_created_at": None,
|
|
2070
|
+
"stream_created_at": None,
|
|
2071
|
+
"last_ddl": None,
|
|
2072
|
+
},
|
|
2073
|
+
)
|
|
2074
|
+
partitioned_sources[db][schema].append({"entity": entity, "identity": identity})
|
|
2075
|
+
fqn_to_parts[identity] = (db, schema, entity)
|
|
2076
|
+
|
|
2077
|
+
if not partitioned_sources:
|
|
2078
|
+
return source_types
|
|
2079
|
+
|
|
2080
|
+
state_queries: list[str] = []
|
|
2081
|
+
for db, schemas in partitioned_sources.items():
|
|
2082
|
+
select_rows: list[str] = []
|
|
2083
|
+
for schema, tables in schemas.items():
|
|
2084
|
+
for table_info in tables:
|
|
2085
|
+
select_rows.append(
|
|
2086
|
+
"SELECT "
|
|
2087
|
+
f"{IdentityParser.to_sql_value(db)} AS catalog_name, "
|
|
2088
|
+
f"{IdentityParser.to_sql_value(schema)} AS schema_name, "
|
|
2089
|
+
f"{IdentityParser.to_sql_value(table_info['entity'])} AS table_name"
|
|
2090
|
+
)
|
|
2091
|
+
|
|
2092
|
+
if not select_rows:
|
|
2093
|
+
continue
|
|
2094
|
+
|
|
2095
|
+
target_entities_clause = "\n UNION ALL\n ".join(select_rows)
|
|
2096
|
+
# Main query:
|
|
2097
|
+
# 1. Enumerate the target tables via target_entities.
|
|
2098
|
+
# 2. Pull their metadata (last_altered, type) from INFORMATION_SCHEMA.TABLES.
|
|
2099
|
+
# 3. Look up the most recent stream activity for those FQNs only.
|
|
2100
|
+
# 4. Capture creation timestamps and use last_ddl vs created_at to classify each target,
|
|
2101
|
+
# so we mark tables as stale when they were recreated even if column hashes still match.
|
|
2102
|
+
state_queries.append(
|
|
2103
|
+
f"""WITH target_entities AS (
|
|
2104
|
+
{target_entities_clause}
|
|
2105
|
+
),
|
|
2106
|
+
table_info AS (
|
|
2107
|
+
SELECT
|
|
2108
|
+
{app_name}.api.normalize_fq_ids(
|
|
2109
|
+
ARRAY_CONSTRUCT(
|
|
2110
|
+
CASE
|
|
2111
|
+
WHEN t.table_catalog = UPPER(t.table_catalog) THEN t.table_catalog
|
|
2112
|
+
ELSE '"' || t.table_catalog || '"'
|
|
2113
|
+
END || '.' ||
|
|
2114
|
+
CASE
|
|
2115
|
+
WHEN t.table_schema = UPPER(t.table_schema) THEN t.table_schema
|
|
2116
|
+
ELSE '"' || t.table_schema || '"'
|
|
2117
|
+
END || '.' ||
|
|
2118
|
+
CASE
|
|
2119
|
+
WHEN t.table_name = UPPER(t.table_name) THEN t.table_name
|
|
2120
|
+
ELSE '"' || t.table_name || '"'
|
|
2121
|
+
END
|
|
2122
|
+
)
|
|
2123
|
+
)[0]:identifier::string AS fqn,
|
|
2124
|
+
CONVERT_TIMEZONE('UTC', t.last_altered) AS last_ddl,
|
|
2125
|
+
CONVERT_TIMEZONE('UTC', t.created) AS table_created_at,
|
|
2126
|
+
t.table_type AS kind
|
|
2127
|
+
FROM {db}.INFORMATION_SCHEMA.tables t
|
|
2128
|
+
JOIN target_entities te
|
|
2129
|
+
ON t.table_catalog = te.catalog_name
|
|
2130
|
+
AND t.table_schema = te.schema_name
|
|
2131
|
+
AND t.table_name = te.table_name
|
|
2132
|
+
),
|
|
2133
|
+
stream_activity AS (
|
|
2134
|
+
SELECT
|
|
2135
|
+
sa.fqn,
|
|
2136
|
+
MAX(sa.created_at) AS created_at
|
|
2137
|
+
FROM (
|
|
2138
|
+
SELECT
|
|
2139
|
+
{app_name}.api.normalize_fq_ids(ARRAY_CONSTRUCT(fq_object_name))[0]:identifier::string AS fqn,
|
|
2140
|
+
created_at
|
|
2141
|
+
FROM {app_name}.api.data_streams
|
|
2142
|
+
WHERE rai_database = '{PYREL_ROOT_DB}'
|
|
2143
|
+
) sa
|
|
2144
|
+
JOIN table_info ti
|
|
2145
|
+
ON sa.fqn = ti.fqn
|
|
2146
|
+
GROUP BY sa.fqn
|
|
2147
|
+
)
|
|
2077
2148
|
SELECT
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
)
|
|
2093
|
-
)[0]:identifier::string) as FQN,
|
|
2094
|
-
CONVERT_TIMEZONE('UTC', LAST_DDL) AS LAST_DDL,
|
|
2095
|
-
TABLE_TYPE as KIND,
|
|
2096
|
-
SHA2(LISTAGG(
|
|
2097
|
-
COLUMN_NAME ||
|
|
2098
|
-
CASE
|
|
2099
|
-
WHEN c.NUMERIC_PRECISION IS NOT NULL AND c.NUMERIC_SCALE IS NOT NULL
|
|
2100
|
-
THEN c.DATA_TYPE || '(' || c.NUMERIC_PRECISION || ',' || c.NUMERIC_SCALE || ')'
|
|
2101
|
-
WHEN c.DATETIME_PRECISION IS NOT NULL
|
|
2102
|
-
THEN c.DATA_TYPE || '(0,' || c.DATETIME_PRECISION || ')'
|
|
2103
|
-
WHEN c.CHARACTER_MAXIMUM_LENGTH IS NOT NULL
|
|
2104
|
-
THEN c.DATA_TYPE || '(' || c.CHARACTER_MAXIMUM_LENGTH || ')'
|
|
2105
|
-
ELSE c.DATA_TYPE
|
|
2106
|
-
END ||
|
|
2107
|
-
IS_NULLABLE,
|
|
2108
|
-
','
|
|
2109
|
-
) WITHIN GROUP (ORDER BY COLUMN_NAME), 256) as COLUMNS_HASH
|
|
2110
|
-
FROM {db}.INFORMATION_SCHEMA.TABLES t
|
|
2111
|
-
JOIN {db}.INFORMATION_SCHEMA.COLUMNS c
|
|
2112
|
-
ON t.TABLE_CATALOG = c.TABLE_CATALOG
|
|
2113
|
-
AND t.TABLE_SCHEMA = c.TABLE_SCHEMA
|
|
2114
|
-
AND t.TABLE_NAME = c.TABLE_NAME
|
|
2115
|
-
WHERE t.TABLE_CATALOG = {IdentityParser.to_sql_value(db)} AND ({" OR ".join(
|
|
2116
|
-
f"(t.TABLE_SCHEMA = {IdentityParser.to_sql_value(schema)} AND t.TABLE_NAME IN ({','.join(f'{IdentityParser.to_sql_value(table)}' for table in tables)}))"
|
|
2117
|
-
for schema, tables in schemas.items()
|
|
2118
|
-
)})
|
|
2119
|
-
GROUP BY t.TABLE_CATALOG, t.TABLE_SCHEMA, t.TABLE_NAME, t.LAST_DDL, t.TABLE_TYPE
|
|
2120
|
-
) inf on inf.FQN = ds.FQ_OBJECT_NAME
|
|
2121
|
-
"""
|
|
2122
|
-
for db, schemas in partitioned_sources.items()
|
|
2149
|
+
ti.fqn,
|
|
2150
|
+
ti.kind,
|
|
2151
|
+
ti.last_ddl,
|
|
2152
|
+
ti.table_created_at,
|
|
2153
|
+
sa.created_at AS stream_created_at,
|
|
2154
|
+
IFF(
|
|
2155
|
+
DATEDIFF(second, sa.created_at::timestamp, ti.last_ddl::timestamp) > 0,
|
|
2156
|
+
'STALE',
|
|
2157
|
+
'CURRENT'
|
|
2158
|
+
) AS state
|
|
2159
|
+
FROM table_info ti
|
|
2160
|
+
LEFT JOIN stream_activity sa
|
|
2161
|
+
ON sa.fqn = ti.fqn
|
|
2162
|
+
"""
|
|
2123
2163
|
)
|
|
2124
|
-
|
|
2164
|
+
|
|
2165
|
+
stale_fqns: list[str] = []
|
|
2166
|
+
for state_query in state_queries:
|
|
2167
|
+
for row in self._exec(state_query):
|
|
2168
|
+
row_dict = row.as_dict() if hasattr(row, "as_dict") else dict(row)
|
|
2169
|
+
row_fqn = row_dict["FQN"]
|
|
2170
|
+
parser = IdentityParser(row_fqn, True)
|
|
2171
|
+
fqn = parser.identity
|
|
2172
|
+
assert fqn, f"Error parsing returned FQN: {row_fqn}"
|
|
2173
|
+
|
|
2174
|
+
source_types[fqn]["type"] = (
|
|
2175
|
+
"TABLE" if row_dict["KIND"] == "BASE TABLE" else row_dict["KIND"]
|
|
2176
|
+
)
|
|
2177
|
+
source_types[fqn]["state"] = row_dict["STATE"]
|
|
2178
|
+
source_types[fqn]["last_ddl"] = normalize_datetime(row_dict.get("LAST_DDL"))
|
|
2179
|
+
source_types[fqn]["table_created_at"] = normalize_datetime(row_dict.get("TABLE_CREATED_AT"))
|
|
2180
|
+
source_types[fqn]["stream_created_at"] = normalize_datetime(row_dict.get("STREAM_CREATED_AT"))
|
|
2181
|
+
if row_dict["STATE"] == "STALE":
|
|
2182
|
+
stale_fqns.append(fqn)
|
|
2183
|
+
|
|
2184
|
+
if not stale_fqns:
|
|
2185
|
+
return source_types
|
|
2186
|
+
|
|
2187
|
+
# We batch stale tables by database/schema so each Snowflake query can hash
|
|
2188
|
+
# multiple objects at once instead of issuing one statement per table.
|
|
2189
|
+
stale_partitioned: dict[str, dict[str, list[dict[str, str]]]] = defaultdict(
|
|
2190
|
+
lambda: defaultdict(list)
|
|
2125
2191
|
)
|
|
2192
|
+
for fqn in stale_fqns:
|
|
2193
|
+
db, schema, table = fqn_to_parts[fqn]
|
|
2194
|
+
stale_partitioned[db][schema].append({"table": table, "identity": fqn})
|
|
2195
|
+
|
|
2196
|
+
# Build one hash query per database, grouping schemas/tables inside so we submit
|
|
2197
|
+
# at most a handful of set-based statements to Snowflake.
|
|
2198
|
+
for db, schemas in stale_partitioned.items():
|
|
2199
|
+
column_select_rows: list[str] = []
|
|
2200
|
+
for schema, tables in schemas.items():
|
|
2201
|
+
for table_info in tables:
|
|
2202
|
+
# Build the literal rows for this db/schema so we can join back
|
|
2203
|
+
# against INFORMATION_SCHEMA.COLUMNS in a single statement.
|
|
2204
|
+
column_select_rows.append(
|
|
2205
|
+
"SELECT "
|
|
2206
|
+
f"{IdentityParser.to_sql_value(db)} AS catalog_name, "
|
|
2207
|
+
f"{IdentityParser.to_sql_value(schema)} AS schema_name, "
|
|
2208
|
+
f"{IdentityParser.to_sql_value(table_info['table'])} AS table_name"
|
|
2209
|
+
)
|
|
2126
2210
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
parser = IdentityParser(row_fqn, True)
|
|
2130
|
-
fqn = parser.identity
|
|
2131
|
-
assert fqn, f"Error parsing returned FQN: {row_fqn}"
|
|
2211
|
+
if not column_select_rows:
|
|
2212
|
+
continue
|
|
2132
2213
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2214
|
+
target_entities_clause = "\n UNION ALL\n ".join(column_select_rows)
|
|
2215
|
+
# Main query: compute deterministic column hashes for every stale table
|
|
2216
|
+
# in this database/schema batch so we can compare schemas without a round trip per table.
|
|
2217
|
+
column_query = f"""WITH target_entities AS (
|
|
2218
|
+
{target_entities_clause}
|
|
2219
|
+
),
|
|
2220
|
+
column_info AS (
|
|
2221
|
+
SELECT
|
|
2222
|
+
{app_name}.api.normalize_fq_ids(
|
|
2223
|
+
ARRAY_CONSTRUCT(
|
|
2224
|
+
CASE
|
|
2225
|
+
WHEN c.table_catalog = UPPER(c.table_catalog) THEN c.table_catalog
|
|
2226
|
+
ELSE '"' || c.table_catalog || '"'
|
|
2227
|
+
END || '.' ||
|
|
2228
|
+
CASE
|
|
2229
|
+
WHEN c.table_schema = UPPER(c.table_schema) THEN c.table_schema
|
|
2230
|
+
ELSE '"' || c.table_schema || '"'
|
|
2231
|
+
END || '.' ||
|
|
2232
|
+
CASE
|
|
2233
|
+
WHEN c.table_name = UPPER(c.table_name) THEN c.table_name
|
|
2234
|
+
ELSE '"' || c.table_name || '"'
|
|
2235
|
+
END
|
|
2236
|
+
)
|
|
2237
|
+
)[0]:identifier::string AS fqn,
|
|
2238
|
+
c.column_name,
|
|
2239
|
+
CASE
|
|
2240
|
+
WHEN c.numeric_precision IS NOT NULL AND c.numeric_scale IS NOT NULL
|
|
2241
|
+
THEN c.data_type || '(' || c.numeric_precision || ',' || c.numeric_scale || ')'
|
|
2242
|
+
WHEN c.datetime_precision IS NOT NULL
|
|
2243
|
+
THEN c.data_type || '(0,' || c.datetime_precision || ')'
|
|
2244
|
+
WHEN c.character_maximum_length IS NOT NULL
|
|
2245
|
+
THEN c.data_type || '(' || c.character_maximum_length || ')'
|
|
2246
|
+
ELSE c.data_type
|
|
2247
|
+
END AS type_signature,
|
|
2248
|
+
IFF(c.is_nullable = 'YES', 'YES', 'NO') AS nullable_flag
|
|
2249
|
+
FROM {db}.INFORMATION_SCHEMA.COLUMNS c
|
|
2250
|
+
JOIN target_entities te
|
|
2251
|
+
ON c.table_catalog = te.catalog_name
|
|
2252
|
+
AND c.table_schema = te.schema_name
|
|
2253
|
+
AND c.table_name = te.table_name
|
|
2254
|
+
)
|
|
2255
|
+
SELECT
|
|
2256
|
+
fqn,
|
|
2257
|
+
HEX_ENCODE(
|
|
2258
|
+
HASH_AGG(
|
|
2259
|
+
HASH(
|
|
2260
|
+
column_name,
|
|
2261
|
+
type_signature,
|
|
2262
|
+
nullable_flag
|
|
2263
|
+
)
|
|
2264
|
+
)
|
|
2265
|
+
) AS columns_hash
|
|
2266
|
+
FROM column_info
|
|
2267
|
+
GROUP BY fqn
|
|
2268
|
+
"""
|
|
2269
|
+
|
|
2270
|
+
for row in self._exec(column_query):
|
|
2271
|
+
row_fqn = row["FQN"]
|
|
2272
|
+
parser = IdentityParser(row_fqn, True)
|
|
2273
|
+
fqn = parser.identity
|
|
2274
|
+
assert fqn, f"Error parsing returned FQN: {row_fqn}"
|
|
2275
|
+
source_types[fqn]["columns_hash"] = row["COLUMNS_HASH"]
|
|
2136
2276
|
|
|
2137
2277
|
return source_types
|
|
2138
2278
|
|
|
@@ -2142,12 +2282,13 @@ Otherwise, remove it from your '{profile}' configuration profile.
|
|
|
2142
2282
|
invalid_sources = {}
|
|
2143
2283
|
source_references = []
|
|
2144
2284
|
for source, info in source_info.items():
|
|
2145
|
-
|
|
2285
|
+
source_type = info.get("type")
|
|
2286
|
+
if source_type is None:
|
|
2146
2287
|
missing_sources.append(source)
|
|
2147
|
-
elif
|
|
2148
|
-
invalid_sources[source] =
|
|
2288
|
+
elif source_type not in ("TABLE", "VIEW"):
|
|
2289
|
+
invalid_sources[source] = source_type
|
|
2149
2290
|
else:
|
|
2150
|
-
source_references.append(f"{app_name}.api.object_reference('{
|
|
2291
|
+
source_references.append(f"{app_name}.api.object_reference('{source_type}', '{source}')")
|
|
2151
2292
|
|
|
2152
2293
|
if missing_sources:
|
|
2153
2294
|
current_role = self.get_sf_session().get_current_role()
|
|
@@ -2831,7 +2972,16 @@ class SnowflakeClient(Client):
|
|
|
2831
2972
|
|
|
2832
2973
|
query_attrs_dict = json.loads(headers.get("X-Query-Attributes", "{}")) if headers else {}
|
|
2833
2974
|
with debugging.span("poll_use_index", sources=self.resources.sources, model=model, engine=engine_name, **query_attrs_dict):
|
|
2834
|
-
self.poll_use_index(
|
|
2975
|
+
self.poll_use_index(
|
|
2976
|
+
app_name=app_name,
|
|
2977
|
+
sources=self.resources.sources,
|
|
2978
|
+
model=model,
|
|
2979
|
+
engine_name=engine_name,
|
|
2980
|
+
engine_size=engine_size,
|
|
2981
|
+
language="rel",
|
|
2982
|
+
program_span_id=program_span_id,
|
|
2983
|
+
headers=headers
|
|
2984
|
+
)
|
|
2835
2985
|
|
|
2836
2986
|
self.last_database_version = len(self.resources.sources)
|
|
2837
2987
|
self._manage_packages()
|
|
@@ -2850,12 +3000,20 @@ class SnowflakeClient(Client):
|
|
|
2850
3000
|
model: str,
|
|
2851
3001
|
engine_name: str,
|
|
2852
3002
|
engine_size: str | None = None,
|
|
3003
|
+
language: str = "rel",
|
|
2853
3004
|
program_span_id: str | None = None,
|
|
2854
3005
|
headers: Dict | None = None,
|
|
2855
3006
|
):
|
|
2856
3007
|
assert isinstance(self.resources, Resources)
|
|
2857
3008
|
return self.resources.poll_use_index(
|
|
2858
|
-
app_name,
|
|
3009
|
+
app_name=app_name,
|
|
3010
|
+
sources=sources,
|
|
3011
|
+
model=model,
|
|
3012
|
+
engine_name=engine_name,
|
|
3013
|
+
engine_size=engine_size,
|
|
3014
|
+
language=language,
|
|
3015
|
+
program_span_id=program_span_id,
|
|
3016
|
+
headers=headers
|
|
2859
3017
|
)
|
|
2860
3018
|
|
|
2861
3019
|
|
|
@@ -3045,6 +3203,7 @@ class DirectAccessResources(Resources):
|
|
|
3045
3203
|
headers: Dict[str, str] | None = None,
|
|
3046
3204
|
path_params: Dict[str, str] | None = None,
|
|
3047
3205
|
query_params: Dict[str, str] | None = None,
|
|
3206
|
+
skip_auto_create: bool = False,
|
|
3048
3207
|
) -> requests.Response:
|
|
3049
3208
|
with debugging.span("direct_access_request"):
|
|
3050
3209
|
def _send_request():
|
|
@@ -3066,7 +3225,8 @@ class DirectAccessResources(Resources):
|
|
|
3066
3225
|
)
|
|
3067
3226
|
|
|
3068
3227
|
# fix engine on engine error and retry
|
|
3069
|
-
if
|
|
3228
|
+
# Skip auto-retry if skip_auto_create is True to avoid recursion
|
|
3229
|
+
if _is_engine_issue(message) and not skip_auto_create:
|
|
3070
3230
|
engine = payload.get("engine_name", "") if payload else ""
|
|
3071
3231
|
self.auto_create_engine(engine)
|
|
3072
3232
|
response = _send_request()
|
|
@@ -3161,6 +3321,7 @@ class DirectAccessResources(Resources):
|
|
|
3161
3321
|
model: str,
|
|
3162
3322
|
engine_name: str,
|
|
3163
3323
|
engine_size: str = "",
|
|
3324
|
+
language: str = "rel",
|
|
3164
3325
|
rai_relations: List[str] | None = None,
|
|
3165
3326
|
pyrel_program_id: str | None = None,
|
|
3166
3327
|
skip_pull_relations: bool = False,
|
|
@@ -3176,6 +3337,7 @@ class DirectAccessResources(Resources):
|
|
|
3176
3337
|
payload = {
|
|
3177
3338
|
"model_name": model,
|
|
3178
3339
|
"caller_engine_name": engine_name,
|
|
3340
|
+
"language": language,
|
|
3179
3341
|
"pyrel_program_id": pyrel_program_id,
|
|
3180
3342
|
"skip_pull_relations": skip_pull_relations,
|
|
3181
3343
|
"rai_relations": rai_relations or [],
|
|
@@ -3201,6 +3363,7 @@ class DirectAccessResources(Resources):
|
|
|
3201
3363
|
model: str,
|
|
3202
3364
|
engine_name: str,
|
|
3203
3365
|
engine_size: str | None = None,
|
|
3366
|
+
language: str = "rel",
|
|
3204
3367
|
program_span_id: str | None = None,
|
|
3205
3368
|
headers: Dict | None = None,
|
|
3206
3369
|
):
|
|
@@ -3211,6 +3374,7 @@ class DirectAccessResources(Resources):
|
|
|
3211
3374
|
model=model,
|
|
3212
3375
|
engine_name=engine_name,
|
|
3213
3376
|
engine_size=engine_size,
|
|
3377
|
+
language=language,
|
|
3214
3378
|
program_span_id=program_span_id,
|
|
3215
3379
|
headers=headers,
|
|
3216
3380
|
generation=self.generation,
|
|
@@ -3351,14 +3515,14 @@ class DirectAccessResources(Resources):
|
|
|
3351
3515
|
with debugging.span("create_model", dbname=name):
|
|
3352
3516
|
return self._create_database(name,"")
|
|
3353
3517
|
|
|
3354
|
-
def delete_graph(self, name:str, force=False):
|
|
3518
|
+
def delete_graph(self, name:str, force=False, language: str = "rel"):
|
|
3355
3519
|
prop_hdrs = debugging.gen_current_propagation_headers()
|
|
3356
3520
|
if self.config.get("use_graph_index", USE_GRAPH_INDEX):
|
|
3357
3521
|
keep_database = not force and self.config.get("reuse_model", True)
|
|
3358
|
-
with debugging.span("release_index", name=name, keep_database=keep_database):
|
|
3522
|
+
with debugging.span("release_index", name=name, keep_database=keep_database, language=language):
|
|
3359
3523
|
response = self.request(
|
|
3360
3524
|
"release_index",
|
|
3361
|
-
payload={"model_name": name, "keep_database": keep_database},
|
|
3525
|
+
payload={"model_name": name, "keep_database": keep_database, "language": language},
|
|
3362
3526
|
headers=prop_hdrs,
|
|
3363
3527
|
)
|
|
3364
3528
|
if (
|
|
@@ -3431,7 +3595,7 @@ class DirectAccessResources(Resources):
|
|
|
3431
3595
|
return sorted(engines, key=lambda x: x["name"])
|
|
3432
3596
|
|
|
3433
3597
|
def get_engine(self, name: str):
|
|
3434
|
-
response = self.request("get_engine", path_params={"engine_name": name, "engine_type": "logic"})
|
|
3598
|
+
response = self.request("get_engine", path_params={"engine_name": name, "engine_type": "logic"}, skip_auto_create=True)
|
|
3435
3599
|
if response.status_code == 404: # engine not found return 404
|
|
3436
3600
|
return None
|
|
3437
3601
|
elif response.status_code != 200:
|
|
@@ -3478,6 +3642,7 @@ class DirectAccessResources(Resources):
|
|
|
3478
3642
|
payload=payload,
|
|
3479
3643
|
path_params={"engine_type": "logic"},
|
|
3480
3644
|
headers=headers,
|
|
3645
|
+
skip_auto_create=True,
|
|
3481
3646
|
)
|
|
3482
3647
|
if response.status_code != 200:
|
|
3483
3648
|
raise ResponseStatusException(
|
|
@@ -3489,6 +3654,7 @@ class DirectAccessResources(Resources):
|
|
|
3489
3654
|
"delete_engine",
|
|
3490
3655
|
path_params={"engine_name": name, "engine_type": "logic"},
|
|
3491
3656
|
headers=headers,
|
|
3657
|
+
skip_auto_create=True,
|
|
3492
3658
|
)
|
|
3493
3659
|
if response.status_code != 200:
|
|
3494
3660
|
raise ResponseStatusException(
|
|
@@ -3499,6 +3665,7 @@ class DirectAccessResources(Resources):
|
|
|
3499
3665
|
response = self.request(
|
|
3500
3666
|
"suspend_engine",
|
|
3501
3667
|
path_params={"engine_name": name, "engine_type": "logic"},
|
|
3668
|
+
skip_auto_create=True,
|
|
3502
3669
|
)
|
|
3503
3670
|
if response.status_code != 200:
|
|
3504
3671
|
raise ResponseStatusException(
|
|
@@ -3510,6 +3677,7 @@ class DirectAccessResources(Resources):
|
|
|
3510
3677
|
"resume_engine",
|
|
3511
3678
|
path_params={"engine_name": name, "engine_type": "logic"},
|
|
3512
3679
|
headers=headers,
|
|
3680
|
+
skip_auto_create=True,
|
|
3513
3681
|
)
|
|
3514
3682
|
if response.status_code != 200:
|
|
3515
3683
|
raise ResponseStatusException(
|
relationalai/clients/types.py
CHANGED
|
@@ -38,10 +38,13 @@ class EngineState(TypedDict):
|
|
|
38
38
|
auto_suspend: int|None
|
|
39
39
|
suspends_at: datetime|None
|
|
40
40
|
|
|
41
|
-
class SourceInfo(TypedDict):
|
|
41
|
+
class SourceInfo(TypedDict, total=False):
|
|
42
42
|
type: str|None
|
|
43
43
|
state: str
|
|
44
44
|
columns_hash: str|None
|
|
45
|
+
table_created_at: datetime|None
|
|
46
|
+
stream_created_at: datetime|None
|
|
47
|
+
last_ddl: datetime|None
|
|
45
48
|
source: str
|
|
46
49
|
|
|
47
50
|
|