mlrun 1.10.0rc40__py3-none-any.whl → 1.11.0rc16__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.
Potentially problematic release.
This version of mlrun might be problematic. Click here for more details.
- mlrun/__init__.py +3 -2
- mlrun/__main__.py +0 -4
- mlrun/artifacts/dataset.py +2 -2
- mlrun/artifacts/plots.py +1 -1
- mlrun/{model_monitoring/db/tsdb/tdengine → auth}/__init__.py +2 -3
- mlrun/auth/nuclio.py +89 -0
- mlrun/auth/providers.py +429 -0
- mlrun/auth/utils.py +415 -0
- mlrun/common/constants.py +7 -0
- mlrun/common/model_monitoring/helpers.py +41 -4
- mlrun/common/runtimes/constants.py +28 -0
- mlrun/common/schemas/__init__.py +13 -3
- mlrun/common/schemas/alert.py +2 -2
- mlrun/common/schemas/api_gateway.py +3 -0
- mlrun/common/schemas/auth.py +10 -10
- mlrun/common/schemas/client_spec.py +4 -0
- mlrun/common/schemas/constants.py +25 -0
- mlrun/common/schemas/frontend_spec.py +1 -8
- mlrun/common/schemas/function.py +24 -0
- mlrun/common/schemas/hub.py +3 -2
- mlrun/common/schemas/model_monitoring/__init__.py +1 -1
- mlrun/common/schemas/model_monitoring/constants.py +2 -2
- mlrun/common/schemas/secret.py +17 -2
- mlrun/common/secrets.py +95 -1
- mlrun/common/types.py +10 -10
- mlrun/config.py +53 -15
- mlrun/data_types/infer.py +2 -2
- mlrun/datastore/__init__.py +2 -3
- mlrun/datastore/base.py +274 -10
- mlrun/datastore/datastore.py +1 -1
- mlrun/datastore/datastore_profile.py +49 -17
- mlrun/datastore/model_provider/huggingface_provider.py +6 -2
- mlrun/datastore/model_provider/model_provider.py +2 -2
- mlrun/datastore/model_provider/openai_provider.py +2 -2
- mlrun/datastore/s3.py +15 -16
- mlrun/datastore/sources.py +1 -1
- mlrun/datastore/store_resources.py +4 -4
- mlrun/datastore/storeytargets.py +16 -10
- mlrun/datastore/targets.py +1 -1
- mlrun/datastore/utils.py +16 -3
- mlrun/datastore/v3io.py +1 -1
- mlrun/db/base.py +36 -12
- mlrun/db/httpdb.py +316 -101
- mlrun/db/nopdb.py +29 -11
- mlrun/errors.py +4 -2
- mlrun/execution.py +11 -12
- mlrun/feature_store/api.py +1 -1
- mlrun/feature_store/common.py +1 -1
- mlrun/feature_store/feature_vector_utils.py +1 -1
- mlrun/feature_store/steps.py +8 -6
- mlrun/frameworks/_common/utils.py +3 -3
- mlrun/frameworks/_dl_common/loggers/logger.py +1 -1
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +2 -1
- mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +1 -1
- mlrun/frameworks/_ml_common/utils.py +2 -1
- mlrun/frameworks/auto_mlrun/auto_mlrun.py +4 -3
- mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +2 -1
- mlrun/frameworks/onnx/dataset.py +2 -1
- mlrun/frameworks/onnx/mlrun_interface.py +2 -1
- mlrun/frameworks/pytorch/callbacks/logging_callback.py +5 -4
- mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +2 -1
- mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +2 -1
- mlrun/frameworks/pytorch/utils.py +2 -1
- mlrun/frameworks/sklearn/metric.py +2 -1
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +5 -4
- mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +2 -1
- mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +2 -1
- mlrun/hub/__init__.py +37 -0
- mlrun/hub/base.py +142 -0
- mlrun/hub/module.py +67 -76
- mlrun/hub/step.py +113 -0
- mlrun/launcher/base.py +2 -1
- mlrun/launcher/local.py +2 -1
- mlrun/model.py +12 -2
- mlrun/model_monitoring/__init__.py +0 -1
- mlrun/model_monitoring/api.py +2 -2
- mlrun/model_monitoring/applications/base.py +20 -6
- mlrun/model_monitoring/applications/context.py +1 -0
- mlrun/model_monitoring/controller.py +7 -17
- mlrun/model_monitoring/db/_schedules.py +2 -16
- mlrun/model_monitoring/db/_stats.py +2 -13
- mlrun/model_monitoring/db/tsdb/__init__.py +9 -7
- mlrun/model_monitoring/db/tsdb/base.py +2 -4
- mlrun/model_monitoring/db/tsdb/preaggregate.py +234 -0
- mlrun/model_monitoring/db/tsdb/stream_graph_steps.py +63 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_metrics_queries.py +414 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_predictions_queries.py +376 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_results_queries.py +590 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connection.py +434 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connector.py +541 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_operations.py +808 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_schema.py +502 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream.py +163 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream_graph_steps.py +60 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_dataframe_processor.py +141 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_query_builder.py +585 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/writer_graph_steps.py +73 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +4 -6
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +147 -79
- mlrun/model_monitoring/features_drift_table.py +2 -1
- mlrun/model_monitoring/helpers.py +2 -1
- mlrun/model_monitoring/stream_processing.py +18 -16
- mlrun/model_monitoring/writer.py +4 -3
- mlrun/package/__init__.py +2 -1
- mlrun/platforms/__init__.py +0 -44
- mlrun/platforms/iguazio.py +1 -1
- mlrun/projects/operations.py +11 -10
- mlrun/projects/project.py +81 -82
- mlrun/run.py +4 -7
- mlrun/runtimes/__init__.py +2 -204
- mlrun/runtimes/base.py +89 -21
- mlrun/runtimes/constants.py +225 -0
- mlrun/runtimes/daskjob.py +4 -2
- mlrun/runtimes/databricks_job/databricks_runtime.py +2 -1
- mlrun/runtimes/mounts.py +5 -0
- mlrun/runtimes/nuclio/__init__.py +12 -8
- mlrun/runtimes/nuclio/api_gateway.py +36 -6
- mlrun/runtimes/nuclio/application/application.py +200 -32
- mlrun/runtimes/nuclio/function.py +154 -49
- mlrun/runtimes/nuclio/serving.py +55 -42
- mlrun/runtimes/pod.py +59 -10
- mlrun/secrets.py +46 -2
- mlrun/serving/__init__.py +2 -0
- mlrun/serving/remote.py +5 -5
- mlrun/serving/routers.py +3 -3
- mlrun/serving/server.py +46 -43
- mlrun/serving/serving_wrapper.py +6 -2
- mlrun/serving/states.py +554 -207
- mlrun/serving/steps.py +1 -1
- mlrun/serving/system_steps.py +42 -33
- mlrun/track/trackers/mlflow_tracker.py +29 -31
- mlrun/utils/helpers.py +89 -16
- mlrun/utils/http.py +9 -2
- mlrun/utils/notifications/notification/git.py +1 -1
- mlrun/utils/notifications/notification/mail.py +39 -16
- mlrun/utils/notifications/notification_pusher.py +2 -2
- mlrun/utils/version/version.json +2 -2
- mlrun/utils/version/version.py +3 -4
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/METADATA +39 -49
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/RECORD +144 -130
- mlrun/db/auth_utils.py +0 -152
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +0 -343
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +0 -75
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +0 -281
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +0 -1368
- mlrun/model_monitoring/db/tsdb/tdengine/writer_graph_steps.py +0 -51
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
# Copyright 2025 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
import psycopg
|
|
18
|
+
|
|
19
|
+
import mlrun.common.schemas.model_monitoring as mm_schemas
|
|
20
|
+
import mlrun.errors
|
|
21
|
+
import mlrun.model_monitoring.db.tsdb.timescaledb.timescaledb_schema as timescaledb_schema
|
|
22
|
+
from mlrun.datastore.datastore_profile import DatastoreProfilePostgreSQL
|
|
23
|
+
from mlrun.model_monitoring.db.tsdb.preaggregate import PreAggregateConfig
|
|
24
|
+
from mlrun.model_monitoring.db.tsdb.timescaledb.timescaledb_connection import (
|
|
25
|
+
Statement,
|
|
26
|
+
TimescaleDBConnection,
|
|
27
|
+
)
|
|
28
|
+
from mlrun.model_monitoring.db.tsdb.timescaledb.utils.timescaledb_query_builder import (
|
|
29
|
+
TimescaleDBNaming,
|
|
30
|
+
)
|
|
31
|
+
from mlrun.utils import datetime_from_iso, logger
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TimescaleDBOperationsManager:
|
|
35
|
+
"""
|
|
36
|
+
Handles all CRUD operations for TimescaleDB TSDB connector.
|
|
37
|
+
|
|
38
|
+
This class implements all create/update/delete operations for model monitoring data:
|
|
39
|
+
- Table and schema creation with optional pre-aggregates and continuous aggregates
|
|
40
|
+
- Event writing with parameterized queries
|
|
41
|
+
- Record deletion with support for both raw and aggregate data cleanup
|
|
42
|
+
- Resource deletion with automatic discovery of project-related tables and views
|
|
43
|
+
- Schema management with automatic cleanup of empty schemas
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Key Features:
|
|
47
|
+
- Parameterized queries for all write/delete operations
|
|
48
|
+
- Automatic discovery of aggregate tables for comprehensive cleanup
|
|
49
|
+
- Transaction-based operations for data consistency
|
|
50
|
+
- Configurable pre-aggregation with retention policies
|
|
51
|
+
- Thread-safe operations through shared connection pooling
|
|
52
|
+
|
|
53
|
+
:param project: Project name used for table naming and schema organization
|
|
54
|
+
:param connection: Shared TimescaleDBConnection instance
|
|
55
|
+
:param pre_aggregate_config: Optional configuration for pre-aggregated tables
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
project: str,
|
|
61
|
+
connection: TimescaleDBConnection,
|
|
62
|
+
pre_aggregate_config: Optional[PreAggregateConfig] = None,
|
|
63
|
+
profile: Optional[DatastoreProfilePostgreSQL] = None,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize operations handler with a shared connection.
|
|
67
|
+
|
|
68
|
+
:param project: The project name
|
|
69
|
+
:param connection: Shared TimescaleDBConnection instance
|
|
70
|
+
:param pre_aggregate_config: Optional pre-aggregation configuration
|
|
71
|
+
:param profile: Optional datastore profile for admin operations (database creation)
|
|
72
|
+
"""
|
|
73
|
+
self.project = project
|
|
74
|
+
self._pre_aggregate_config = pre_aggregate_config
|
|
75
|
+
self._profile = profile
|
|
76
|
+
|
|
77
|
+
# Use the injected shared connection
|
|
78
|
+
self._connection = connection
|
|
79
|
+
|
|
80
|
+
# Initialize table schemas
|
|
81
|
+
self._init_tables()
|
|
82
|
+
|
|
83
|
+
def _init_tables(self) -> None:
|
|
84
|
+
self.tables = timescaledb_schema.create_table_schemas(self.project)
|
|
85
|
+
|
|
86
|
+
def _create_db_if_not_exists(self) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Create the database if it does not exist.
|
|
89
|
+
|
|
90
|
+
This method connects to the default 'postgres' database to create
|
|
91
|
+
the monitoring database if it doesn't already exist. It also ensures the
|
|
92
|
+
TimescaleDB extension is enabled in the monitoring database.
|
|
93
|
+
|
|
94
|
+
Note: Requires a profile to be set during initialization.
|
|
95
|
+
"""
|
|
96
|
+
if not self._profile:
|
|
97
|
+
logger.debug(
|
|
98
|
+
"No profile provided, skipping database creation",
|
|
99
|
+
project=self.project,
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
database_name = self._profile.database
|
|
104
|
+
|
|
105
|
+
logger.debug(
|
|
106
|
+
"Checking/creating TimescaleDB database",
|
|
107
|
+
project=self.project,
|
|
108
|
+
database=database_name,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Connect to default postgres database to create the monitoring database
|
|
112
|
+
admin_connection = TimescaleDBConnection(
|
|
113
|
+
dsn=self._profile.admin_dsn(),
|
|
114
|
+
min_connections=1,
|
|
115
|
+
max_connections=1,
|
|
116
|
+
autocommit=True, # DDL requires autocommit
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Check if database exists using parameterized Statement
|
|
121
|
+
check_stmt = Statement(
|
|
122
|
+
sql="SELECT 1 FROM pg_database WHERE datname = %s",
|
|
123
|
+
parameters=(database_name,),
|
|
124
|
+
)
|
|
125
|
+
result = admin_connection.run(query=check_stmt)
|
|
126
|
+
|
|
127
|
+
if not result or not result.data:
|
|
128
|
+
# Database doesn't exist, create it
|
|
129
|
+
# Note: CREATE DATABASE cannot be parameterized, but database_name
|
|
130
|
+
# comes from our own profile, not user input
|
|
131
|
+
admin_connection.run(statements=[f'CREATE DATABASE "{database_name}"'])
|
|
132
|
+
logger.info(
|
|
133
|
+
"Created TimescaleDB database",
|
|
134
|
+
project=self.project,
|
|
135
|
+
database=database_name,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
logger.debug(
|
|
139
|
+
"TimescaleDB database already exists",
|
|
140
|
+
project=self.project,
|
|
141
|
+
database=database_name,
|
|
142
|
+
)
|
|
143
|
+
finally:
|
|
144
|
+
# Close the admin connection pool to avoid resource leak
|
|
145
|
+
admin_connection.close()
|
|
146
|
+
|
|
147
|
+
def create_tables(
|
|
148
|
+
self, pre_aggregate_config: Optional[PreAggregateConfig] = None
|
|
149
|
+
) -> None:
|
|
150
|
+
config = pre_aggregate_config or self._pre_aggregate_config
|
|
151
|
+
|
|
152
|
+
logger.debug(
|
|
153
|
+
"Creating TimescaleDB tables for model monitoring",
|
|
154
|
+
project=self.project,
|
|
155
|
+
with_pre_aggregates=config is not None,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Create database if it doesn't exist
|
|
159
|
+
self._create_db_if_not_exists()
|
|
160
|
+
|
|
161
|
+
# Try to create extension, ignore if already exists
|
|
162
|
+
try:
|
|
163
|
+
self._connection.run(
|
|
164
|
+
statements=["CREATE EXTENSION IF NOT EXISTS timescaledb"]
|
|
165
|
+
)
|
|
166
|
+
except psycopg.errors.DuplicateObject:
|
|
167
|
+
# Extension already loaded - this is fine
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
# Create schema if it doesn't exist
|
|
171
|
+
schema_name = self.tables[mm_schemas.TimescaleDBTables.PREDICTIONS].schema
|
|
172
|
+
self._connection.run(statements=[f"CREATE SCHEMA IF NOT EXISTS {schema_name}"])
|
|
173
|
+
|
|
174
|
+
# Create main tables and convert to hypertables
|
|
175
|
+
for table_type, table in self.tables.items():
|
|
176
|
+
statements = [table._create_table_query()]
|
|
177
|
+
|
|
178
|
+
# Convert to hypertable
|
|
179
|
+
statements.append(table._create_hypertable_query())
|
|
180
|
+
|
|
181
|
+
# Create indexes
|
|
182
|
+
statements.extend(table._create_indexes_query())
|
|
183
|
+
|
|
184
|
+
# Create pre-aggregate tables if config provided
|
|
185
|
+
if config:
|
|
186
|
+
statements.extend(table._create_continuous_aggregates_query(config))
|
|
187
|
+
statements.extend(table._create_retention_policies_query(config))
|
|
188
|
+
|
|
189
|
+
# Execute all statements for this table
|
|
190
|
+
self._connection.run(statements=statements)
|
|
191
|
+
|
|
192
|
+
logger.debug(
|
|
193
|
+
"Successfully created TimescaleDB tables",
|
|
194
|
+
project=self.project,
|
|
195
|
+
table_count=len(self.tables),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def write_application_event(
|
|
199
|
+
self,
|
|
200
|
+
event: dict,
|
|
201
|
+
kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Write a single result or metric to TimescaleDB using parameterized queries.
|
|
205
|
+
|
|
206
|
+
Uses PostgreSQL's parameterized queries for safety and performance.
|
|
207
|
+
|
|
208
|
+
:param event: Event data to write
|
|
209
|
+
:param kind: Type of event (RESULT or METRIC)
|
|
210
|
+
"""
|
|
211
|
+
if kind == mm_schemas.WriterEventKind.RESULT:
|
|
212
|
+
table = self.tables[mm_schemas.TimescaleDBTables.APP_RESULTS]
|
|
213
|
+
else:
|
|
214
|
+
table = self.tables[mm_schemas.TimescaleDBTables.METRICS]
|
|
215
|
+
|
|
216
|
+
# Convert datetime strings to datetime objects if needed
|
|
217
|
+
for time_field in [
|
|
218
|
+
mm_schemas.WriterEvent.END_INFER_TIME,
|
|
219
|
+
mm_schemas.WriterEvent.START_INFER_TIME,
|
|
220
|
+
]:
|
|
221
|
+
if time_field in event:
|
|
222
|
+
if isinstance(event[time_field], str):
|
|
223
|
+
event[time_field] = datetime_from_iso(event[time_field])
|
|
224
|
+
# datetime objects can stay as-is
|
|
225
|
+
|
|
226
|
+
# Prepare the INSERT statement with parameterized query
|
|
227
|
+
columns = list(table.columns.keys())
|
|
228
|
+
placeholders = ", ".join(["%s"] * len(columns))
|
|
229
|
+
|
|
230
|
+
insert_sql = f"""
|
|
231
|
+
INSERT INTO {table.full_name()} ({', '.join(columns)})
|
|
232
|
+
VALUES ({placeholders})
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
# Prepare values in the correct order
|
|
236
|
+
values = tuple(event.get(col) for col in columns)
|
|
237
|
+
|
|
238
|
+
# Create parameterized statement
|
|
239
|
+
stmt = Statement(insert_sql, values)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# Execute parameterized query
|
|
243
|
+
self._connection.run(statements=[stmt])
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(
|
|
246
|
+
"Failed to write application event to TimescaleDB",
|
|
247
|
+
project=self.project,
|
|
248
|
+
table=table.table_name,
|
|
249
|
+
error=mlrun.errors.err_to_str(e),
|
|
250
|
+
)
|
|
251
|
+
raise mlrun.errors.MLRunRuntimeError(
|
|
252
|
+
f"Failed to write event to TimescaleDB: {e}"
|
|
253
|
+
) from e
|
|
254
|
+
|
|
255
|
+
def delete_tsdb_records(
|
|
256
|
+
self,
|
|
257
|
+
endpoint_ids: list[str],
|
|
258
|
+
include_aggregates: bool = True,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Delete model endpoint records from TimescaleDB using parameterized queries.
|
|
262
|
+
|
|
263
|
+
:param endpoint_ids: List of endpoint IDs to delete
|
|
264
|
+
:param include_aggregates: Whether to delete from pre-aggregate tables as well
|
|
265
|
+
"""
|
|
266
|
+
if not endpoint_ids:
|
|
267
|
+
logger.debug("No endpoint IDs provided for deletion", project=self.project)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
logger.debug(
|
|
271
|
+
"Deleting model endpoint records from TimescaleDB",
|
|
272
|
+
project=self.project,
|
|
273
|
+
number_of_endpoints_to_delete=len(endpoint_ids),
|
|
274
|
+
include_aggregates=include_aggregates,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
# Execute all deletions in a single transaction to prevent race conditions
|
|
279
|
+
# Raw data must be deleted first to prevent continuous aggregates from repopulating
|
|
280
|
+
all_deletion_statements = []
|
|
281
|
+
|
|
282
|
+
# 1. Delete raw data first (removes source for continuous aggregates)
|
|
283
|
+
all_deletion_statements.extend(
|
|
284
|
+
self._get_raw_delete_statements(endpoint_ids)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# 2. Delete aggregate data second (cleanup existing aggregated data)
|
|
288
|
+
if include_aggregates:
|
|
289
|
+
# Always try to discover and delete aggregates, regardless of config
|
|
290
|
+
all_deletion_statements.extend(
|
|
291
|
+
self._get_aggregate_delete_statements_by_endpoints(endpoint_ids)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Execute all deletions in a single transaction
|
|
295
|
+
self._connection.run(statements=all_deletion_statements)
|
|
296
|
+
|
|
297
|
+
logger.debug(
|
|
298
|
+
"Successfully deleted model endpoint records from TimescaleDB",
|
|
299
|
+
project=self.project,
|
|
300
|
+
number_of_endpoints_deleted=len(endpoint_ids),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(
|
|
305
|
+
"Failed to delete model endpoint records from TimescaleDB",
|
|
306
|
+
project=self.project,
|
|
307
|
+
endpoint_count=len(endpoint_ids),
|
|
308
|
+
error=mlrun.errors.err_to_str(e),
|
|
309
|
+
)
|
|
310
|
+
raise
|
|
311
|
+
|
|
312
|
+
def _get_raw_delete_statements(self, endpoint_ids: list[str]) -> list[Statement]:
|
|
313
|
+
"""
|
|
314
|
+
Get parameterized DELETE statements for raw data tables.
|
|
315
|
+
|
|
316
|
+
:param endpoint_ids: List of endpoint IDs to delete
|
|
317
|
+
:return: List of Statement objects for raw data deletion
|
|
318
|
+
"""
|
|
319
|
+
statements = []
|
|
320
|
+
|
|
321
|
+
for table_schema in self.tables.values():
|
|
322
|
+
if len(endpoint_ids) == 1:
|
|
323
|
+
delete_sql = (
|
|
324
|
+
f"DELETE FROM {table_schema.full_name()} "
|
|
325
|
+
f"WHERE {mm_schemas.WriterEvent.ENDPOINT_ID} = %s"
|
|
326
|
+
)
|
|
327
|
+
stmt = Statement(delete_sql, (endpoint_ids[0],))
|
|
328
|
+
else:
|
|
329
|
+
delete_sql = (
|
|
330
|
+
f"DELETE FROM {table_schema.full_name()} "
|
|
331
|
+
f"WHERE {mm_schemas.WriterEvent.ENDPOINT_ID} = ANY(%s)"
|
|
332
|
+
)
|
|
333
|
+
stmt = Statement(delete_sql, (endpoint_ids,))
|
|
334
|
+
|
|
335
|
+
statements.append(stmt)
|
|
336
|
+
|
|
337
|
+
return statements
|
|
338
|
+
|
|
339
|
+
def _get_aggregate_delete_statements_by_endpoints(
|
|
340
|
+
self, endpoint_ids: list[str]
|
|
341
|
+
) -> list[Statement]:
|
|
342
|
+
"""
|
|
343
|
+
Get parameterized DELETE statements for aggregate data tables by discovering existing tables.
|
|
344
|
+
|
|
345
|
+
This approach discovers all existing aggregate tables rather than relying on configuration,
|
|
346
|
+
ensuring we don't miss any aggregate data.
|
|
347
|
+
|
|
348
|
+
:param endpoint_ids: List of endpoint IDs to delete (must be non-empty)
|
|
349
|
+
:return: List of Statement objects for aggregate data deletion
|
|
350
|
+
"""
|
|
351
|
+
statements = []
|
|
352
|
+
|
|
353
|
+
# Early return for empty endpoint list - nothing to delete
|
|
354
|
+
if not endpoint_ids:
|
|
355
|
+
return statements
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
schema_name = self.tables[mm_schemas.TimescaleDBTables.PREDICTIONS].schema
|
|
359
|
+
|
|
360
|
+
# Get base table patterns for tables that have endpoint_id
|
|
361
|
+
base_patterns = []
|
|
362
|
+
base_patterns.extend(
|
|
363
|
+
self.tables[table_type].table_name
|
|
364
|
+
for table_type in [
|
|
365
|
+
mm_schemas.TimescaleDBTables.PREDICTIONS,
|
|
366
|
+
mm_schemas.TimescaleDBTables.METRICS,
|
|
367
|
+
mm_schemas.TimescaleDBTables.APP_RESULTS,
|
|
368
|
+
]
|
|
369
|
+
if table_type in self.tables
|
|
370
|
+
)
|
|
371
|
+
if not base_patterns:
|
|
372
|
+
return statements
|
|
373
|
+
|
|
374
|
+
# Build query to find all aggregate tables and continuous aggregate views
|
|
375
|
+
# TimescaleDB continuous aggregates appear as VIEWs in information_schema
|
|
376
|
+
pattern_conditions = []
|
|
377
|
+
parameters = [schema_name]
|
|
378
|
+
|
|
379
|
+
for pattern in base_patterns:
|
|
380
|
+
pattern_conditions.extend(
|
|
381
|
+
[
|
|
382
|
+
"table_name LIKE %s", # _agg_ tables
|
|
383
|
+
"table_name LIKE %s", # _cagg_ views
|
|
384
|
+
]
|
|
385
|
+
)
|
|
386
|
+
parameters.extend(TimescaleDBNaming.get_all_aggregate_patterns(pattern))
|
|
387
|
+
|
|
388
|
+
discovery_stmt = Statement(
|
|
389
|
+
f"""
|
|
390
|
+
SELECT table_name
|
|
391
|
+
FROM information_schema.tables
|
|
392
|
+
WHERE table_schema = %s
|
|
393
|
+
AND table_type IN ('BASE TABLE', 'VIEW')
|
|
394
|
+
AND ({' OR '.join(pattern_conditions)})
|
|
395
|
+
ORDER BY table_name
|
|
396
|
+
""",
|
|
397
|
+
tuple(parameters),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
result = self._connection.run(query=discovery_stmt)
|
|
401
|
+
discovered_objects = (
|
|
402
|
+
[row[0] for row in result.data] if result and result.data else []
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if not discovered_objects:
|
|
406
|
+
logger.debug(
|
|
407
|
+
"No aggregate objects found for deletion",
|
|
408
|
+
project=self.project,
|
|
409
|
+
schema=schema_name,
|
|
410
|
+
)
|
|
411
|
+
return statements
|
|
412
|
+
|
|
413
|
+
logger.debug(
|
|
414
|
+
"Discovered aggregate objects for endpoint deletion",
|
|
415
|
+
project=self.project,
|
|
416
|
+
aggregate_objects=len(discovered_objects),
|
|
417
|
+
endpoint_count=len(endpoint_ids),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Create delete statements for all discovered aggregate objects
|
|
421
|
+
for object_name in discovered_objects:
|
|
422
|
+
delete_sql = f"DELETE FROM {schema_name}.{object_name} WHERE"
|
|
423
|
+
if len(endpoint_ids) == 1:
|
|
424
|
+
delete_sql += f" {mm_schemas.WriterEvent.ENDPOINT_ID} = %s"
|
|
425
|
+
stmt = Statement(delete_sql, (endpoint_ids[0],))
|
|
426
|
+
else:
|
|
427
|
+
delete_sql += f" {mm_schemas.WriterEvent.ENDPOINT_ID} = ANY(%s)"
|
|
428
|
+
stmt = Statement(delete_sql, (endpoint_ids,))
|
|
429
|
+
|
|
430
|
+
statements.append(stmt)
|
|
431
|
+
|
|
432
|
+
except Exception as e:
|
|
433
|
+
logger.debug(
|
|
434
|
+
"Failed to discover aggregate objects for deletion",
|
|
435
|
+
project=self.project,
|
|
436
|
+
error=mlrun.errors.err_to_str(e),
|
|
437
|
+
)
|
|
438
|
+
# Continue with empty statements list rather than failing completely
|
|
439
|
+
|
|
440
|
+
return statements
|
|
441
|
+
|
|
442
|
+
def delete_tsdb_resources(self) -> None:
|
|
443
|
+
"""
|
|
444
|
+
Delete all project resources in TimescaleDB by discovering existing tables that match our patterns.
|
|
445
|
+
|
|
446
|
+
This approach ensures we don't miss any tables, even if configurations are out of sync.
|
|
447
|
+
"""
|
|
448
|
+
logger.debug(
|
|
449
|
+
"Deleting all project resources from TimescaleDB",
|
|
450
|
+
project=self.project,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
schema_name = self.tables[mm_schemas.TimescaleDBTables.PREDICTIONS].schema
|
|
455
|
+
|
|
456
|
+
# Get the base table patterns for this project
|
|
457
|
+
base_patterns = []
|
|
458
|
+
base_patterns.extend(
|
|
459
|
+
table_schema.table_name for table_schema in self.tables.values()
|
|
460
|
+
)
|
|
461
|
+
# Build discovery query for all project objects
|
|
462
|
+
pattern_conditions = []
|
|
463
|
+
parameters = [schema_name]
|
|
464
|
+
|
|
465
|
+
for pattern in base_patterns:
|
|
466
|
+
# Match exact table name OR table name with _agg_/_cagg_ suffix
|
|
467
|
+
pattern_conditions.extend(
|
|
468
|
+
[
|
|
469
|
+
"table_name = %s",
|
|
470
|
+
"table_name LIKE %s", # _agg_ tables
|
|
471
|
+
"table_name LIKE %s", # _cagg_ views
|
|
472
|
+
]
|
|
473
|
+
)
|
|
474
|
+
parameters.extend(TimescaleDBNaming.get_deletion_patterns(pattern))
|
|
475
|
+
|
|
476
|
+
# Discover tables
|
|
477
|
+
tables_stmt = Statement(
|
|
478
|
+
f"""
|
|
479
|
+
SELECT table_name
|
|
480
|
+
FROM information_schema.tables
|
|
481
|
+
WHERE table_schema = %s
|
|
482
|
+
AND table_type = 'BASE TABLE'
|
|
483
|
+
AND ({' OR '.join(pattern_conditions)})
|
|
484
|
+
ORDER BY table_name
|
|
485
|
+
""",
|
|
486
|
+
tuple([schema_name] + parameters[1:]),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Build separate pattern conditions for TimescaleDB continuous aggregates
|
|
490
|
+
view_pattern_conditions = []
|
|
491
|
+
view_parameters = [schema_name]
|
|
492
|
+
|
|
493
|
+
for pattern in base_patterns:
|
|
494
|
+
# For continuous aggregates, look for _cagg_ pattern
|
|
495
|
+
view_pattern_conditions.append("view_name LIKE %s")
|
|
496
|
+
view_parameters.append(TimescaleDBNaming.get_cagg_pattern(pattern))
|
|
497
|
+
|
|
498
|
+
# Discover TimescaleDB continuous aggregates (use TimescaleDB catalog, not pg_matviews)
|
|
499
|
+
views_stmt = Statement(
|
|
500
|
+
f"""
|
|
501
|
+
SELECT view_name as table_name
|
|
502
|
+
FROM timescaledb_information.continuous_aggregates
|
|
503
|
+
WHERE view_schema = %s
|
|
504
|
+
AND ({' OR '.join(view_pattern_conditions)})
|
|
505
|
+
ORDER BY view_name
|
|
506
|
+
""",
|
|
507
|
+
tuple(view_parameters),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
tables_result = self._connection.run(query=tables_stmt)
|
|
511
|
+
views_result = self._connection.run(query=views_stmt)
|
|
512
|
+
|
|
513
|
+
discovered_tables = (
|
|
514
|
+
[row[0] for row in tables_result.data]
|
|
515
|
+
if tables_result and tables_result.data
|
|
516
|
+
else []
|
|
517
|
+
)
|
|
518
|
+
discovered_views = (
|
|
519
|
+
[row[0] for row in views_result.data]
|
|
520
|
+
if views_result and views_result.data
|
|
521
|
+
else []
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
if not discovered_tables and not discovered_views:
|
|
525
|
+
logger.debug(
|
|
526
|
+
"No project resources found to delete",
|
|
527
|
+
project=self.project,
|
|
528
|
+
schema=schema_name,
|
|
529
|
+
)
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
logger.debug(
|
|
533
|
+
"Discovered project resources for deletion",
|
|
534
|
+
project=self.project,
|
|
535
|
+
tables=len(discovered_tables),
|
|
536
|
+
views=len(discovered_views),
|
|
537
|
+
schema=schema_name,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
drop_statements = []
|
|
541
|
+
|
|
542
|
+
# Drop materialized views first (they depend on tables)
|
|
543
|
+
if discovered_views:
|
|
544
|
+
view_list = ", ".join(
|
|
545
|
+
f"{schema_name}.{view_name}" for view_name in discovered_views
|
|
546
|
+
)
|
|
547
|
+
drop_statements.append(
|
|
548
|
+
f"DROP MATERIALIZED VIEW IF EXISTS {view_list} CASCADE"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Drop tables second (one by one due to TimescaleDB hypertable limitations)
|
|
552
|
+
drop_statements.extend(
|
|
553
|
+
f"DROP TABLE IF EXISTS {schema_name}.{table_name} CASCADE"
|
|
554
|
+
for table_name in discovered_tables
|
|
555
|
+
)
|
|
556
|
+
# Execute all drops
|
|
557
|
+
if drop_statements:
|
|
558
|
+
self._connection.run(statements=drop_statements)
|
|
559
|
+
|
|
560
|
+
logger.debug(
|
|
561
|
+
"Successfully dropped project resources from TimescaleDB",
|
|
562
|
+
project=self.project,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Optional cleanup: drop schema if empty (errors are logged but don't fail the operation)
|
|
566
|
+
self._drop_schema_if_empty()
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
logger.error(
|
|
570
|
+
"Failed to delete all project resources from TimescaleDB",
|
|
571
|
+
project=self.project,
|
|
572
|
+
error=mlrun.errors.err_to_str(e),
|
|
573
|
+
)
|
|
574
|
+
raise
|
|
575
|
+
|
|
576
|
+
logger.debug(
|
|
577
|
+
"Successfully deleted all project resources from TimescaleDB",
|
|
578
|
+
project=self.project,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def _drop_schema_if_empty(self) -> None:
|
|
582
|
+
"""
|
|
583
|
+
Drop the schema if it contains no more tables using parameterized query.
|
|
584
|
+
|
|
585
|
+
This is a best-effort cleanup operation that should not fail the main resource deletion.
|
|
586
|
+
Schema dropping may fail due to permissions, remaining objects, or concurrent operations,
|
|
587
|
+
but the primary table deletion operation has already succeeded.
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
schema_name = self.tables[mm_schemas.TimescaleDBTables.PREDICTIONS].schema
|
|
591
|
+
|
|
592
|
+
# Check if schema has any tables using parameterized query
|
|
593
|
+
check_stmt = Statement(
|
|
594
|
+
"""
|
|
595
|
+
SELECT COUNT(*) AS table_count
|
|
596
|
+
FROM information_schema.tables
|
|
597
|
+
WHERE table_schema = %s
|
|
598
|
+
""",
|
|
599
|
+
(schema_name,),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
result = self._connection.run(query=check_stmt)
|
|
603
|
+
|
|
604
|
+
if result and result.data and result.data[0][0] == 0:
|
|
605
|
+
# Schema is empty, drop it
|
|
606
|
+
drop_schema_query = f"DROP SCHEMA IF EXISTS {schema_name} CASCADE"
|
|
607
|
+
self._connection.run(statements=[drop_schema_query])
|
|
608
|
+
|
|
609
|
+
logger.debug(
|
|
610
|
+
"Dropped empty schema",
|
|
611
|
+
project=self.project,
|
|
612
|
+
schema=schema_name,
|
|
613
|
+
)
|
|
614
|
+
except Exception as e:
|
|
615
|
+
# Schema dropping is optional cleanup - don't fail the main operation
|
|
616
|
+
# This may happen due to permissions, remaining objects, or concurrent operations
|
|
617
|
+
logger.warning(
|
|
618
|
+
"Failed to check/drop empty schema (non-critical cleanup operation)",
|
|
619
|
+
project=self.project,
|
|
620
|
+
error=mlrun.errors.err_to_str(e),
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def delete_application_records(
|
|
624
|
+
self, application_name: str, endpoint_ids: Optional[list[str]] = None
|
|
625
|
+
) -> None:
|
|
626
|
+
"""
|
|
627
|
+
Delete application records from TimescaleDB for the given model endpoints or all if endpoint_ids is None.
|
|
628
|
+
|
|
629
|
+
This method deletes records from both app_results and metrics tables that match the specified
|
|
630
|
+
application name and optionally filter by endpoint IDs.
|
|
631
|
+
|
|
632
|
+
:param application_name: Name of the application whose records should be deleted
|
|
633
|
+
:param endpoint_ids: Optional list of endpoint IDs to filter deletion. If None, deletes all records
|
|
634
|
+
for the application across all endpoints.
|
|
635
|
+
"""
|
|
636
|
+
logger.debug(
|
|
637
|
+
"Deleting application records from TimescaleDB",
|
|
638
|
+
project=self.project,
|
|
639
|
+
application_name=application_name,
|
|
640
|
+
endpoint_ids=endpoint_ids,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if not application_name:
|
|
644
|
+
logger.warning(
|
|
645
|
+
"No application name provided for deletion", project=self.project
|
|
646
|
+
)
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
self._delete_application_records(application_name, endpoint_ids)
|
|
651
|
+
except Exception as e:
|
|
652
|
+
logger.error(
|
|
653
|
+
"Failed to delete application records from TimescaleDB",
|
|
654
|
+
project=self.project,
|
|
655
|
+
application_name=application_name,
|
|
656
|
+
endpoint_ids=endpoint_ids,
|
|
657
|
+
error=mlrun.errors.err_to_str(e),
|
|
658
|
+
)
|
|
659
|
+
raise mlrun.errors.MLRunRuntimeError(
|
|
660
|
+
f"Failed to delete application records for {application_name}: {e}"
|
|
661
|
+
) from e
|
|
662
|
+
|
|
663
|
+
def _delete_application_records(self, application_name, endpoint_ids):
|
|
664
|
+
base_parameters = [application_name]
|
|
665
|
+
|
|
666
|
+
# Add endpoint filter if provided
|
|
667
|
+
if endpoint_ids:
|
|
668
|
+
if len(endpoint_ids) == 1:
|
|
669
|
+
endpoint_filter = f" AND {mm_schemas.WriterEvent.ENDPOINT_ID} = %s"
|
|
670
|
+
parameters = base_parameters + [endpoint_ids[0]]
|
|
671
|
+
else:
|
|
672
|
+
endpoint_filter = f" AND {mm_schemas.WriterEvent.ENDPOINT_ID} = ANY(%s)"
|
|
673
|
+
parameters = base_parameters + [endpoint_ids]
|
|
674
|
+
else:
|
|
675
|
+
endpoint_filter = ""
|
|
676
|
+
parameters = base_parameters
|
|
677
|
+
|
|
678
|
+
# Delete from app_results table
|
|
679
|
+
app_results_table = self.tables[mm_schemas.TimescaleDBTables.APP_RESULTS]
|
|
680
|
+
app_filter = f"{mm_schemas.WriterEvent.APPLICATION_NAME} = %s"
|
|
681
|
+
app_results_sql = (
|
|
682
|
+
f"DELETE FROM {app_results_table.full_name()} "
|
|
683
|
+
f"WHERE {app_filter}{endpoint_filter}"
|
|
684
|
+
)
|
|
685
|
+
deletion_statements = [Statement(app_results_sql, tuple(parameters))]
|
|
686
|
+
# Delete from metrics table
|
|
687
|
+
metrics_table = self.tables[mm_schemas.TimescaleDBTables.METRICS]
|
|
688
|
+
metrics_sql = (
|
|
689
|
+
f"DELETE FROM {metrics_table.full_name()} "
|
|
690
|
+
f"WHERE {app_filter}{endpoint_filter}"
|
|
691
|
+
)
|
|
692
|
+
deletion_statements.append(Statement(metrics_sql, tuple(parameters)))
|
|
693
|
+
|
|
694
|
+
# Also delete from aggregate tables if they exist
|
|
695
|
+
aggregate_statements = self._get_aggregate_delete_statements_by_application(
|
|
696
|
+
application_name, endpoint_ids
|
|
697
|
+
)
|
|
698
|
+
deletion_statements.extend(aggregate_statements)
|
|
699
|
+
|
|
700
|
+
# Execute all deletions in a single transaction
|
|
701
|
+
self._connection.run(statements=deletion_statements)
|
|
702
|
+
|
|
703
|
+
logger.debug(
|
|
704
|
+
"Successfully deleted application records from TimescaleDB",
|
|
705
|
+
project=self.project,
|
|
706
|
+
application_name=application_name,
|
|
707
|
+
endpoint_count=len(endpoint_ids) if endpoint_ids else "all",
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
def _get_aggregate_delete_statements_by_application(
|
|
711
|
+
self, application_name: str, endpoint_ids: Optional[list[str]] = None
|
|
712
|
+
) -> list[Statement]:
|
|
713
|
+
"""
|
|
714
|
+
Get parameterized DELETE statements for aggregate tables filtered by application name.
|
|
715
|
+
|
|
716
|
+
This discovers existing aggregate tables and creates deletion statements that filter
|
|
717
|
+
by both application name and optionally endpoint IDs.
|
|
718
|
+
|
|
719
|
+
:param application_name: Application name to filter by
|
|
720
|
+
:param endpoint_ids: Optional endpoint IDs to filter by
|
|
721
|
+
:return: List of Statement objects for aggregate data deletion
|
|
722
|
+
"""
|
|
723
|
+
statements = []
|
|
724
|
+
|
|
725
|
+
try:
|
|
726
|
+
schema_name = self.tables[mm_schemas.TimescaleDBTables.PREDICTIONS].schema
|
|
727
|
+
|
|
728
|
+
# Discover all continuous aggregates and materialized views for this project
|
|
729
|
+
discovery_stmt = Statement(
|
|
730
|
+
"""
|
|
731
|
+
SELECT table_name
|
|
732
|
+
FROM (
|
|
733
|
+
SELECT matviewname as table_name
|
|
734
|
+
FROM pg_matviews
|
|
735
|
+
WHERE schemaname = %s
|
|
736
|
+
AND matviewname LIKE %s
|
|
737
|
+
|
|
738
|
+
UNION ALL
|
|
739
|
+
|
|
740
|
+
SELECT view_name as table_name
|
|
741
|
+
FROM timescaledb_information.continuous_aggregates
|
|
742
|
+
WHERE view_schema = %s
|
|
743
|
+
AND view_name LIKE %s
|
|
744
|
+
) AS combined_objects
|
|
745
|
+
ORDER BY table_name
|
|
746
|
+
""",
|
|
747
|
+
(schema_name, f"%{self.project}%", schema_name, f"%{self.project}%"),
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
result = self._connection.run(query=discovery_stmt)
|
|
751
|
+
discovered_objects = (
|
|
752
|
+
[row[0] for row in result.data] if result and result.data else []
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if not discovered_objects:
|
|
756
|
+
logger.debug(
|
|
757
|
+
"No aggregate objects found for application deletion",
|
|
758
|
+
project=self.project,
|
|
759
|
+
application_name=application_name,
|
|
760
|
+
schema=schema_name,
|
|
761
|
+
)
|
|
762
|
+
return statements
|
|
763
|
+
|
|
764
|
+
logger.debug(
|
|
765
|
+
"Discovered aggregate objects for application deletion",
|
|
766
|
+
project=self.project,
|
|
767
|
+
application_name=application_name,
|
|
768
|
+
aggregate_objects=len(discovered_objects),
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
# Build filter conditions
|
|
772
|
+
app_filter = f"{mm_schemas.WriterEvent.APPLICATION_NAME} = %s"
|
|
773
|
+
base_parameters = [application_name]
|
|
774
|
+
|
|
775
|
+
# Note: None means "delete all for this application" (no endpoint filter)
|
|
776
|
+
# Empty list [] would delete nothing, so we treat it same as None
|
|
777
|
+
if endpoint_ids: # Non-empty list
|
|
778
|
+
if len(endpoint_ids) == 1:
|
|
779
|
+
endpoint_filter = f" AND {mm_schemas.WriterEvent.ENDPOINT_ID} = %s"
|
|
780
|
+
parameters = base_parameters + [endpoint_ids[0]]
|
|
781
|
+
else:
|
|
782
|
+
endpoint_filter = (
|
|
783
|
+
f" AND {mm_schemas.WriterEvent.ENDPOINT_ID} = ANY(%s)"
|
|
784
|
+
)
|
|
785
|
+
parameters = base_parameters + [endpoint_ids]
|
|
786
|
+
else:
|
|
787
|
+
endpoint_filter = ""
|
|
788
|
+
parameters = base_parameters
|
|
789
|
+
|
|
790
|
+
# Create delete statements for all discovered aggregate objects
|
|
791
|
+
for object_name in discovered_objects:
|
|
792
|
+
delete_sql = (
|
|
793
|
+
f"DELETE FROM {schema_name}.{object_name} "
|
|
794
|
+
f"WHERE {app_filter}{endpoint_filter}"
|
|
795
|
+
)
|
|
796
|
+
stmt = Statement(delete_sql, tuple(parameters))
|
|
797
|
+
statements.append(stmt)
|
|
798
|
+
|
|
799
|
+
except Exception as e:
|
|
800
|
+
logger.warning(
|
|
801
|
+
"Failed to discover aggregate objects for application deletion",
|
|
802
|
+
project=self.project,
|
|
803
|
+
application_name=application_name,
|
|
804
|
+
error=mlrun.errors.err_to_str(e),
|
|
805
|
+
)
|
|
806
|
+
# Continue with empty statements list rather than failing completely
|
|
807
|
+
|
|
808
|
+
return statements
|