Skip to content

Commit edc3901

Browse files
Merge branch 'main' into fix/centralized-timeseries-cleanup-on-entity-delete
2 parents f6e0670 + af7ffe7 commit edc3901

7 files changed

Lines changed: 612 additions & 73 deletions

File tree

ingestion/src/metadata/ingestion/source/dashboard/qliksense/client.py

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
Websocket Auth & Client for QlikSense
1313
"""
1414
import json
15+
import re
1516
import traceback
1617
from pathlib import Path
17-
from typing import Dict, List, Optional
18+
from typing import Dict, List, Optional, Set
1819

1920
from pydantic import ValidationError
2021

@@ -28,16 +29,22 @@
2829
CREATE_SHEET_SESSION,
2930
GET_DOCS_LIST_REQ,
3031
GET_LOADMODEL_LAYOUT,
32+
GET_SCRIPT,
3133
GET_SHEET_LAYOUT,
34+
GET_TABLES_AND_KEYS,
3235
OPEN_DOC_REQ,
3336
)
3437
from metadata.ingestion.source.dashboard.qliksense.models import (
3538
QlikDashboard,
3639
QlikDashboardResult,
3740
QlikDataModelResult,
41+
QlikFields,
42+
QlikScriptResult,
3843
QlikSheet,
3944
QlikSheetResult,
4045
QlikTable,
46+
QlikTableConnectionProp,
47+
QlikTablesAndKeysResponse,
4148
)
4249
from metadata.utils.constants import UTF_8
4350
from metadata.utils.helpers import clean_uri
@@ -174,26 +181,129 @@ def get_dashboard_charts(self, dashboard_id: str) -> List[QlikSheet]:
174181
logger.warning("Failed to fetch the dashboard charts")
175182
return []
176183

184+
def _get_tables_via_get_tables_and_keys(self) -> Optional[List[QlikTable]]:
185+
"""
186+
Fetch all tables using GetTablesAndKeys API.
187+
This returns all tables in the app including those
188+
created via load scripts, not just Data Manager tables.
189+
"""
190+
resp = self._websocket_send_request(GET_TABLES_AND_KEYS, response=True)
191+
data = QlikTablesAndKeysResponse(**resp)
192+
if not data.result or not data.result.qtr:
193+
return None
194+
tables = []
195+
for table_record in data.result.qtr:
196+
fields = [
197+
QlikFields(
198+
name=field.qName,
199+
id=field.qOriginalFieldName or field.qName,
200+
)
201+
for field in table_record.qFields or []
202+
]
203+
tables.append(
204+
QlikTable(
205+
tableName=table_record.qName,
206+
id=table_record.qName,
207+
connectorProperties=table_record.qConnectorProperties
208+
or QlikTableConnectionProp(),
209+
fields=fields,
210+
)
211+
)
212+
return tables
213+
214+
def _get_tables_via_load_model(self) -> List[QlikTable]:
215+
"""
216+
Fallback: fetch tables from the LoadModel object.
217+
Only returns tables created via Data Manager.
218+
"""
219+
self._websocket_send_request(APP_LOADMODEL_REQ)
220+
models = self._websocket_send_request(GET_LOADMODEL_LAYOUT, response=True)
221+
data_models = QlikDataModelResult(**models)
222+
layout = data_models.result.qLayout
223+
if isinstance(layout, list):
224+
tables = []
225+
for layout in data_models.result.qLayout:
226+
tables.extend(layout.value.tables)
227+
return tables
228+
return layout.tables
229+
177230
def get_dashboard_models(self) -> List[QlikTable]:
178231
"""
179-
Get dahsboard chart list
232+
Get all data model tables for the current app.
233+
Uses GetTablesAndKeys to capture all tables including
234+
those created via load scripts.
235+
Falls back to LoadModel if GetTablesAndKeys fails.
180236
"""
181237
try:
182-
self._websocket_send_request(APP_LOADMODEL_REQ)
183-
models = self._websocket_send_request(GET_LOADMODEL_LAYOUT, response=True)
184-
data_models = QlikDataModelResult(**models)
185-
layout = data_models.result.qLayout
186-
if isinstance(layout, list):
187-
tables = []
188-
for layout in data_models.result.qLayout:
189-
tables.extend(layout.value.tables)
238+
tables = self._get_tables_via_get_tables_and_keys()
239+
if tables is not None:
190240
return tables
191-
return layout.tables
241+
except Exception:
242+
logger.debug(traceback.format_exc())
243+
logger.warning("GetTablesAndKeys failed, falling back to LoadModel")
244+
try:
245+
return self._get_tables_via_load_model()
192246
except Exception:
193247
logger.debug(traceback.format_exc())
194248
logger.warning("Failed to fetch the dashboard datamodels")
195249
return []
196250

251+
def get_script(self) -> Optional[str]:
252+
"""
253+
Retrieve the load script from the current app
254+
using the GetScript Engine API.
255+
"""
256+
try:
257+
resp = self._websocket_send_request(GET_SCRIPT, response=True)
258+
script_result = QlikScriptResult(**resp)
259+
if script_result.result and script_result.result.qScript:
260+
return script_result.result.qScript
261+
except Exception:
262+
logger.debug(traceback.format_exc())
263+
logger.warning("Failed to fetch the app load script")
264+
return None
265+
266+
def get_script_tables(self) -> Dict[str, Set[str]]:
267+
"""
268+
Parse the load script to extract source SQL tables
269+
for each Qlik table defined in the script.
270+
271+
Returns a mapping of qlik_table_name -> set of source table names
272+
found in FROM/JOIN clauses.
273+
"""
274+
table_source_map: Dict[str, Set[str]] = {}
275+
script = self.get_script()
276+
if not script:
277+
return table_source_map
278+
279+
sections = re.split(r"(?:^|\n)\s*(?:\[([^\]]+)\]|(\w+))\s*:", script)
280+
281+
current_table = None
282+
for i, section in enumerate(sections):
283+
if section is None:
284+
continue
285+
stripped = section.strip()
286+
if not stripped:
287+
continue
288+
if i % 3 in (1, 2):
289+
current_table = stripped
290+
continue
291+
if current_table:
292+
from_join_tables = re.findall(
293+
r"(?:FROM|JOIN)\s+((?:(?:\[[a-zA-Z0-9_ ]+\]|[a-zA-Z0-9_]+)\.)*(?:\[[a-zA-Z0-9_ ]+\]|[a-zA-Z0-9_]+))",
294+
stripped,
295+
re.IGNORECASE,
296+
)
297+
sql_tables = {
298+
re.sub(r"[\[\]]", "", t)
299+
for t in from_join_tables
300+
if "." in re.sub(r"[\[\]]", "", t)
301+
}
302+
if sql_tables:
303+
table_source_map.setdefault(current_table, set()).update(sql_tables)
304+
305+
return table_source_map
306+
197307
def get_dashboard_for_test_connection(self):
198308
try:
199309
self.connect_websocket()

ingestion/src/metadata/ingestion/source/dashboard/qliksense/constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,26 @@
7878
"id": 6,
7979
"jsonrpc": "2.0",
8080
}
81+
82+
83+
GET_TABLES_AND_KEYS = {
84+
"handle": 1,
85+
"method": "GetTablesAndKeys",
86+
"params": [
87+
{"qcx": 1000, "qcy": 1000},
88+
{"qcx": 0, "qcy": 0},
89+
30,
90+
True,
91+
False,
92+
],
93+
"id": 7,
94+
"jsonrpc": "2.0",
95+
}
96+
97+
GET_SCRIPT = {
98+
"handle": 1,
99+
"method": "GetScript",
100+
"params": [],
101+
"id": 8,
102+
"jsonrpc": "2.0",
103+
}

0 commit comments

Comments
 (0)