quillsql 2.1.6__py3-none-any.whl → 2.2.1__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.
- quillsql/__init__.py +2 -1
- quillsql/assets/__init__.py +1 -1
- quillsql/assets/pgtypes.py +696 -2781
- quillsql/core.py +427 -54
- quillsql/db/__init__.py +1 -1
- quillsql/db/bigquery.py +108 -74
- quillsql/db/cached_connection.py +6 -5
- quillsql/db/db_helper.py +36 -17
- quillsql/db/postgres.py +94 -39
- quillsql/error.py +4 -4
- quillsql/utils/__init__.py +2 -1
- quillsql/utils/filters.py +180 -0
- quillsql/utils/pivot_template.py +485 -0
- quillsql/utils/run_query_processes.py +17 -16
- quillsql/utils/schema_conversion.py +6 -3
- quillsql/utils/tenants.py +60 -0
- quillsql-2.2.1.dist-info/METADATA +69 -0
- quillsql-2.2.1.dist-info/RECORD +20 -0
- {quillsql-2.1.6.dist-info → quillsql-2.2.1.dist-info}/WHEEL +1 -1
- quillsql-2.1.6.dist-info/METADATA +0 -72
- quillsql-2.1.6.dist-info/RECORD +0 -17
- {quillsql-2.1.6.dist-info → quillsql-2.2.1.dist-info}/top_level.txt +0 -0
quillsql/core.py
CHANGED
|
@@ -3,15 +3,27 @@ from dotenv import load_dotenv
|
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
5
|
import redis
|
|
6
|
+
from .utils import Filter, convert_custom_filter
|
|
7
|
+
import json
|
|
8
|
+
from enum import Enum
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
from quillsql.db.cached_connection import CachedConnection
|
|
9
|
-
from quillsql.db.db_helper import
|
|
12
|
+
from quillsql.db.db_helper import (
|
|
13
|
+
get_db_credentials,
|
|
14
|
+
get_schema_column_info_by_db,
|
|
15
|
+
get_schema_tables_by_db,
|
|
16
|
+
)
|
|
10
17
|
from quillsql.utils.schema_conversion import convert_type_to_postgres
|
|
11
18
|
from quillsql.utils.run_query_processes import (
|
|
12
19
|
array_to_map,
|
|
13
20
|
remove_fields,
|
|
14
21
|
)
|
|
22
|
+
from quillsql.utils.tenants import extract_tenant_ids
|
|
23
|
+
from quillsql.utils.pivot_template import (
|
|
24
|
+
parse_distinct_values,
|
|
25
|
+
hydrate_pivot_template,
|
|
26
|
+
)
|
|
15
27
|
|
|
16
28
|
load_dotenv()
|
|
17
29
|
|
|
@@ -20,8 +32,11 @@ DEV_HOST = "http://localhost:8080"
|
|
|
20
32
|
PROD_HOST = "https://quill-344421.uc.r.appspot.com"
|
|
21
33
|
HOST = DEV_HOST if ENV == "development" else PROD_HOST
|
|
22
34
|
|
|
35
|
+
SINGLE_TENANT = "QUILL_SINGLE_TENANT"
|
|
36
|
+
ALL_TENANTS = "QUILL_ALL_TENANTS"
|
|
37
|
+
FLAG_TASKS = {'dashboard', 'report', 'item', 'report-info', 'filter-options'}
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
# Quill - Fullstack API Platform for Dashboards and Reporting.
|
|
25
40
|
class Quill:
|
|
26
41
|
def __init__(
|
|
27
42
|
self,
|
|
@@ -32,13 +47,27 @@ class Quill:
|
|
|
32
47
|
metadataServerURL=None,
|
|
33
48
|
cache=None,
|
|
34
49
|
):
|
|
50
|
+
if private_key is None:
|
|
51
|
+
raise ValueError("Private key is required")
|
|
52
|
+
if database_type is None:
|
|
53
|
+
raise ValueError("Database type is required")
|
|
54
|
+
if database_connection_string is None and database_config is None:
|
|
55
|
+
raise ValueError("You must provide either DatabaseConnectionString or DatabaseConfig")
|
|
56
|
+
|
|
35
57
|
# Handles both dsn-style connection strings (eg. "dbname=test password=secret" )
|
|
36
58
|
# as well as url-style connection strings (eg. "postgres://foo@db.com")
|
|
37
|
-
self.baseUrl = metadataServerURL if metadataServerURL
|
|
38
|
-
if database_connection_string
|
|
39
|
-
|
|
59
|
+
self.baseUrl = metadataServerURL if metadataServerURL is not None else HOST
|
|
60
|
+
if database_connection_string is not None:
|
|
61
|
+
self.target_connection = CachedConnection(
|
|
62
|
+
database_type,
|
|
63
|
+
get_db_credentials(database_type, database_connection_string),
|
|
64
|
+
cache,
|
|
65
|
+
True,
|
|
66
|
+
)
|
|
40
67
|
else:
|
|
41
|
-
|
|
68
|
+
self.target_connection = CachedConnection(
|
|
69
|
+
database_type, database_config, cache, False
|
|
70
|
+
)
|
|
42
71
|
self.private_key = private_key
|
|
43
72
|
|
|
44
73
|
def get_cache(self, cache_config):
|
|
@@ -52,99 +81,434 @@ class Quill:
|
|
|
52
81
|
)
|
|
53
82
|
return None
|
|
54
83
|
|
|
55
|
-
def query(
|
|
56
|
-
|
|
84
|
+
def query(
|
|
85
|
+
self,
|
|
86
|
+
tenants,
|
|
87
|
+
metadata,
|
|
88
|
+
flags=None,
|
|
89
|
+
filters: list[Filter] = None,
|
|
90
|
+
admin_enabled: bool = None,
|
|
91
|
+
):
|
|
92
|
+
if not tenants:
|
|
93
|
+
raise ValueError("You may not pass an empty tenants array.")
|
|
94
|
+
if flags and not flags:
|
|
95
|
+
raise ValueError("You may not pass an empty flags array.")
|
|
96
|
+
|
|
97
|
+
responseMetadata = {}
|
|
57
98
|
if not metadata:
|
|
58
|
-
return {"error": "
|
|
99
|
+
return {"error": "Missing metadata.", "status": "error", "data": {}}
|
|
59
100
|
|
|
60
101
|
task = metadata.get("task")
|
|
61
102
|
if not task:
|
|
62
|
-
return {"error": "
|
|
103
|
+
return {"error": "Missing task.", "status": "error", "data": {}}
|
|
63
104
|
|
|
64
105
|
try:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
# Set tenant IDs in the connection
|
|
107
|
+
self.target_connection.tenant_ids = extract_tenant_ids(tenants)
|
|
108
|
+
|
|
109
|
+
# Handle pivot-template task
|
|
110
|
+
if task == "pivot-template":
|
|
111
|
+
# Step 1: Get pivot template and queries from server
|
|
112
|
+
pivot_payload = {
|
|
113
|
+
**metadata,
|
|
114
|
+
"tenants": tenants,
|
|
115
|
+
"flags": flags,
|
|
116
|
+
"adminEnabled": admin_enabled,
|
|
117
|
+
}
|
|
118
|
+
if filters is not None:
|
|
119
|
+
pivot_payload["sdkFilters"] = [
|
|
120
|
+
convert_custom_filter(f) for f in filters
|
|
121
|
+
]
|
|
122
|
+
pivot_template_response = self.post_quill(
|
|
123
|
+
"pivot-template",
|
|
124
|
+
pivot_payload,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if pivot_template_response.get("error"):
|
|
128
|
+
return {
|
|
129
|
+
"status": "error",
|
|
130
|
+
"error": pivot_template_response.get("error"),
|
|
131
|
+
"data": pivot_template_response.get("metadata") or {},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
template = pivot_template_response.get("metadata", {}).get("template")
|
|
135
|
+
config = pivot_template_response.get("metadata", {}).get("config")
|
|
136
|
+
distinct_values_query = pivot_template_response.get("metadata", {}).get("distinctValuesQuery")
|
|
137
|
+
row_count_query = pivot_template_response.get("metadata", {}).get("rowCountQuery")
|
|
138
|
+
|
|
139
|
+
# Step 2: Run the distinct values query to get unique values
|
|
140
|
+
distinct_values = []
|
|
141
|
+
if distinct_values_query:
|
|
142
|
+
distinct_value_results = self.run_queries(
|
|
143
|
+
[distinct_values_query],
|
|
144
|
+
self.target_connection.database_type,
|
|
145
|
+
metadata.get("databaseType"),
|
|
146
|
+
metadata,
|
|
147
|
+
None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Parse distinct values from database results
|
|
151
|
+
distinct_values = parse_distinct_values(
|
|
152
|
+
distinct_value_results["queryResults"][0],
|
|
153
|
+
config.get("databaseType")
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Step 3: Hydrate the template with the distinct values
|
|
157
|
+
try:
|
|
158
|
+
final_query = hydrate_pivot_template(template, distinct_values, config)
|
|
159
|
+
except Exception as err:
|
|
160
|
+
return {
|
|
161
|
+
"status": "error",
|
|
162
|
+
"error": f"Failed to hydrate pivot template: {str(err)}",
|
|
163
|
+
"data": {},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Step 4: Run queries - pivot query and optional row count query
|
|
167
|
+
queries_to_run = [final_query]
|
|
168
|
+
if row_count_query:
|
|
169
|
+
# Hydrate the rowCountQuery with the same distinct values
|
|
170
|
+
hydrated_row_count_query = hydrate_pivot_template(
|
|
171
|
+
row_count_query,
|
|
172
|
+
distinct_values,
|
|
173
|
+
config
|
|
174
|
+
)
|
|
175
|
+
queries_to_run.append(hydrated_row_count_query)
|
|
176
|
+
|
|
177
|
+
final_results = self.run_queries(
|
|
178
|
+
queries_to_run,
|
|
179
|
+
self.target_connection.database_type,
|
|
180
|
+
metadata.get("databaseType"),
|
|
181
|
+
metadata,
|
|
182
|
+
pivot_template_response.get("metadata", {}).get("runQueryConfig")
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
responseMetadata = pivot_template_response.get("metadata") or {}
|
|
186
|
+
# Set rows and fields from first query result (the pivot query)
|
|
187
|
+
if final_results.get("queryResults") and len(final_results["queryResults"]) >= 1:
|
|
188
|
+
query_results = final_results["queryResults"][0]
|
|
189
|
+
if query_results.get("rows"):
|
|
190
|
+
responseMetadata["rows"] = query_results["rows"]
|
|
191
|
+
if query_results.get("fields"):
|
|
192
|
+
responseMetadata["fields"] = query_results["fields"]
|
|
193
|
+
|
|
194
|
+
# Remove internal SDK fields before returning to frontend
|
|
195
|
+
if "template" in responseMetadata:
|
|
196
|
+
del responseMetadata["template"]
|
|
197
|
+
if "distinctValuesQuery" in responseMetadata:
|
|
198
|
+
del responseMetadata["distinctValuesQuery"]
|
|
199
|
+
if "rowCountQuery" in responseMetadata:
|
|
200
|
+
del responseMetadata["rowCountQuery"]
|
|
201
|
+
|
|
73
202
|
return {
|
|
74
|
-
"data":
|
|
75
|
-
"
|
|
203
|
+
"data": responseMetadata,
|
|
204
|
+
"queries": final_results,
|
|
205
|
+
"status": "success",
|
|
76
206
|
}
|
|
207
|
+
|
|
208
|
+
# Handle tenant flags synthesis
|
|
209
|
+
tenant_flags = None
|
|
210
|
+
if (task in FLAG_TASKS and
|
|
211
|
+
tenants[0] != ALL_TENANTS and
|
|
212
|
+
tenants[0] != SINGLE_TENANT
|
|
213
|
+
):
|
|
214
|
+
|
|
215
|
+
response = self.post_quill(
|
|
216
|
+
'tenant-mapped-flags',
|
|
217
|
+
{
|
|
218
|
+
'reportId': metadata.get('reportId') or metadata.get('dashboardItemId'),
|
|
219
|
+
'clientId': metadata.get('clientId'),
|
|
220
|
+
'dashboardName': metadata.get('name'),
|
|
221
|
+
'tenants': tenants,
|
|
222
|
+
'flags': flags,
|
|
223
|
+
'adminEnabled': admin_enabled,
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if response.get('error'):
|
|
228
|
+
return {
|
|
229
|
+
'status': 'error',
|
|
230
|
+
'error': response.get('error'),
|
|
231
|
+
'data': response.get('metadata') or {},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
flag_query_results = self.run_queries(
|
|
235
|
+
response.get('queries'),
|
|
236
|
+
self.target_connection.database_type,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
tenant_flags = [
|
|
240
|
+
{
|
|
241
|
+
'tenantField': tenant_field,
|
|
242
|
+
'flags': list(set(row['quill_flag'] for row in query_result['rows']))
|
|
243
|
+
}
|
|
244
|
+
for tenant_field, query_result in zip(
|
|
245
|
+
response['metadata']['queryOrder'],
|
|
246
|
+
flag_query_results['queryResults']
|
|
247
|
+
)
|
|
248
|
+
]
|
|
249
|
+
elif tenants[0] == SINGLE_TENANT and flags:
|
|
250
|
+
if flags and isinstance(flags[0], dict):
|
|
251
|
+
tenant_flags = [{'tenantField': SINGLE_TENANT, 'flags': flags}]
|
|
252
|
+
else:
|
|
253
|
+
tenant_flags = flags
|
|
254
|
+
|
|
255
|
+
pre_query_results = (
|
|
256
|
+
self.run_queries(
|
|
257
|
+
metadata.get("preQueries"),
|
|
258
|
+
self.target_connection.database_type,
|
|
259
|
+
metadata.get("databaseType"),
|
|
260
|
+
metadata,
|
|
261
|
+
metadata.get("runQueryConfig"),
|
|
262
|
+
)
|
|
263
|
+
if metadata.get("preQueries")
|
|
264
|
+
else {}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if metadata.get("runQueryConfig") and metadata.get("runQueryConfig").get(
|
|
268
|
+
"overridePost"
|
|
269
|
+
):
|
|
270
|
+
return {"data": pre_query_results, "status": "success"}
|
|
77
271
|
view_query = None
|
|
78
|
-
if
|
|
272
|
+
if metadata.get("preQueries"):
|
|
79
273
|
view_query = metadata.get("preQueries")[0]
|
|
274
|
+
pre_query_columns = (
|
|
275
|
+
pre_query_results.get("columns")
|
|
276
|
+
if metadata.get("runQueryConfig")
|
|
277
|
+
and metadata.get("runQueryConfig").get("getColumns")
|
|
278
|
+
else None
|
|
279
|
+
)
|
|
80
280
|
payload = {
|
|
81
281
|
**metadata,
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"viewQuery": view_query
|
|
282
|
+
"tenants": tenants,
|
|
283
|
+
"flags": tenant_flags,
|
|
284
|
+
"viewQuery": view_query,
|
|
285
|
+
"preQueryResultsColumns": pre_query_columns,
|
|
286
|
+
"adminEnabled": admin_enabled,
|
|
85
287
|
}
|
|
288
|
+
if filters is not None:
|
|
289
|
+
payload["sdkFilters"] = [convert_custom_filter(f) for f in filters]
|
|
86
290
|
quill_results = self.post_quill(metadata.get("task"), payload)
|
|
87
291
|
if quill_results.get("error"):
|
|
88
|
-
|
|
89
|
-
|
|
292
|
+
responseMetadata = quill_results.get("metadata")
|
|
293
|
+
response = {
|
|
294
|
+
"error": quill_results.get("error"),
|
|
295
|
+
"status": "error",
|
|
296
|
+
"data": {},
|
|
297
|
+
}
|
|
298
|
+
if responseMetadata:
|
|
299
|
+
response["data"] = responseMetadata
|
|
300
|
+
return response
|
|
301
|
+
|
|
302
|
+
# If there is no metadata in the quill results, create one
|
|
90
303
|
if not quill_results.get("metadata"):
|
|
91
304
|
quill_results["metadata"] = {}
|
|
92
305
|
metadata = quill_results.get("metadata")
|
|
93
|
-
|
|
94
|
-
|
|
306
|
+
responseMetadata = metadata
|
|
307
|
+
results = self.run_queries(
|
|
308
|
+
quill_results.get("queries"),
|
|
309
|
+
self.target_connection.database_type,
|
|
310
|
+
metadata.get("databaseType"),
|
|
311
|
+
metadata,
|
|
312
|
+
metadata.get("runQueryConfig"),
|
|
95
313
|
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
314
|
+
|
|
315
|
+
should_wrap_results = isinstance(results, list) or not results
|
|
316
|
+
if should_wrap_results:
|
|
317
|
+
normalized_results = {
|
|
318
|
+
"queryResults": results if isinstance(results, list) else []
|
|
319
|
+
}
|
|
320
|
+
else:
|
|
321
|
+
normalized_results = results
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
should_wrap_results
|
|
325
|
+
and not normalized_results.get("queryResults")
|
|
326
|
+
and quill_results.get("queries")
|
|
327
|
+
):
|
|
328
|
+
normalized_results["queryResults"] = (
|
|
329
|
+
normalized_results.get("queryResults") or []
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
normalized_results.get("mapped_array")
|
|
334
|
+
and metadata.get("runQueryConfig", {}).get("arrayToMap")
|
|
335
|
+
):
|
|
336
|
+
array_to_map = metadata["runQueryConfig"]["arrayToMap"]
|
|
337
|
+
for array, index in zip(
|
|
338
|
+
normalized_results["mapped_array"],
|
|
339
|
+
range(len(normalized_results["mapped_array"])),
|
|
340
|
+
):
|
|
341
|
+
responseMetadata[array_to_map["arrayName"]][index][array_to_map["field"]] = array
|
|
342
|
+
del normalized_results["mapped_array"]
|
|
343
|
+
|
|
344
|
+
query_results_list = normalized_results.get("queryResults") or []
|
|
345
|
+
if len(query_results_list) == 1:
|
|
346
|
+
query_result = query_results_list[0]
|
|
99
347
|
quill_results["metadata"]["rows"] = query_result.get("rows")
|
|
100
348
|
quill_results["metadata"]["fields"] = query_result.get("fields")
|
|
101
349
|
return {
|
|
102
350
|
"data": quill_results.get("metadata"),
|
|
103
|
-
"queries":
|
|
351
|
+
"queries": normalized_results,
|
|
104
352
|
"status": "success",
|
|
105
353
|
}
|
|
106
354
|
|
|
107
355
|
except Exception as err:
|
|
108
|
-
|
|
356
|
+
if task == "update-view":
|
|
357
|
+
self.post_quill(
|
|
358
|
+
"set-broken-view",
|
|
359
|
+
{
|
|
360
|
+
"table": metadata.get("name"),
|
|
361
|
+
"clientId": metadata.get("clientId"),
|
|
362
|
+
"error": str(err),
|
|
363
|
+
"adminEnabled": admin_enabled,
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
return {
|
|
367
|
+
"error": str(err).splitlines()[0],
|
|
368
|
+
"status": "error",
|
|
369
|
+
"data": responseMetadata,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
def apply_limit(self, query, limit):
|
|
373
|
+
# Simple logic: if query already has a limit, don't add another
|
|
374
|
+
if getattr(self.target_connection, 'database_type', '').lower() == 'mssql':
|
|
375
|
+
import re
|
|
376
|
+
if re.search(r'SELECT TOP \\d+', query, re.IGNORECASE):
|
|
377
|
+
return query
|
|
378
|
+
return re.sub(r'select', f'SELECT TOP {limit}', query, flags=re.IGNORECASE)
|
|
379
|
+
else:
|
|
380
|
+
if 'limit ' in query.lower():
|
|
381
|
+
return query
|
|
382
|
+
return f"{query.rstrip(';')} limit {limit}"
|
|
109
383
|
|
|
110
|
-
def run_queries(
|
|
384
|
+
def run_queries(
|
|
385
|
+
self, queries, pkDatabaseType, databaseType=None, metadata=None, runQueryConfig=None
|
|
386
|
+
):
|
|
111
387
|
results = {}
|
|
112
388
|
if not queries:
|
|
113
389
|
return {"queryResults": []}
|
|
114
390
|
if databaseType and databaseType.lower() != pkDatabaseType.lower():
|
|
115
391
|
return {"dbMismatched": True, "backendDatabaseType": pkDatabaseType}
|
|
116
|
-
if runQueryConfig and runQueryConfig.get("getColumnsForSchema"):
|
|
117
|
-
return {"queryResults": []}
|
|
118
392
|
if runQueryConfig and runQueryConfig.get("arrayToMap"):
|
|
119
|
-
array_to_map(
|
|
120
|
-
queries,
|
|
393
|
+
mapped_array = array_to_map(
|
|
394
|
+
queries,
|
|
395
|
+
runQueryConfig.get("arrayToMap"),
|
|
396
|
+
metadata,
|
|
397
|
+
self.target_connection,
|
|
121
398
|
)
|
|
122
|
-
|
|
399
|
+
|
|
400
|
+
return {"queryResults": [], "mapped_array": mapped_array}
|
|
123
401
|
elif runQueryConfig and runQueryConfig.get("getColumns"):
|
|
124
|
-
query_results = self.target_connection.query(
|
|
125
|
-
|
|
402
|
+
query_results = self.target_connection.query(
|
|
403
|
+
queries[0].strip().rstrip(";") + " limit 1000"
|
|
404
|
+
)
|
|
405
|
+
results["columns"] = [
|
|
406
|
+
{
|
|
407
|
+
"fieldType": convert_type_to_postgres(result["dataTypeID"]),
|
|
408
|
+
"name": result["name"],
|
|
409
|
+
"displayName": result["name"],
|
|
410
|
+
"isVisible": True,
|
|
411
|
+
"field": result["name"],
|
|
412
|
+
}
|
|
413
|
+
for result in query_results["fields"]
|
|
414
|
+
]
|
|
415
|
+
elif runQueryConfig and runQueryConfig.get("getColumnsForSchema"):
|
|
416
|
+
query_results = []
|
|
417
|
+
for table in queries:
|
|
418
|
+
if not table.get("viewQuery") or (
|
|
419
|
+
not table.get("isSelectStar") and not table.get("customFieldInfo")
|
|
420
|
+
):
|
|
421
|
+
query_results.append(table)
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
limit = ""
|
|
425
|
+
if runQueryConfig.get("limitBy"):
|
|
426
|
+
limit = f" limit {runQueryConfig.get('limitBy')}"
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
query_result = self.target_connection.query(
|
|
430
|
+
f"{table['viewQuery'].strip().rstrip(';')} {limit}"
|
|
431
|
+
)
|
|
432
|
+
columns = [
|
|
433
|
+
{
|
|
434
|
+
"fieldType": convert_type_to_postgres(field["dataTypeID"]),
|
|
435
|
+
"name": field["name"],
|
|
436
|
+
"displayName": field["name"],
|
|
437
|
+
"isVisible": True,
|
|
438
|
+
"field": field["name"],
|
|
439
|
+
}
|
|
440
|
+
for field in query_result["fields"]
|
|
441
|
+
]
|
|
442
|
+
query_results.append(
|
|
443
|
+
{**table, "columns": columns, "rows": query_result["rows"]}
|
|
444
|
+
)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
query_results.append(
|
|
447
|
+
{**table, "error": f"Error fetching columns {e}"}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
results["queryResults"] = query_results
|
|
451
|
+
if runQueryConfig.get("fieldsToRemove"):
|
|
452
|
+
results["queryResults"] = [
|
|
453
|
+
{
|
|
454
|
+
**table,
|
|
455
|
+
"columns": [
|
|
456
|
+
column
|
|
457
|
+
for column in table.get("columns", [])
|
|
458
|
+
if column["name"] not in runQueryConfig["fieldsToRemove"]
|
|
459
|
+
],
|
|
460
|
+
}
|
|
461
|
+
for table in query_results
|
|
462
|
+
]
|
|
463
|
+
return results
|
|
126
464
|
elif runQueryConfig and runQueryConfig.get("getTables"):
|
|
127
465
|
tables = get_schema_tables_by_db(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
466
|
+
self.target_connection.database_type,
|
|
467
|
+
self.target_connection.connection,
|
|
468
|
+
runQueryConfig["schemaNames"],
|
|
131
469
|
)
|
|
132
470
|
schema = get_schema_column_info_by_db(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
471
|
+
self.target_connection.database_type,
|
|
472
|
+
self.target_connection.connection,
|
|
473
|
+
runQueryConfig["schemaNames"],
|
|
474
|
+
tables,
|
|
137
475
|
)
|
|
138
476
|
results["queryResults"] = schema
|
|
477
|
+
elif runQueryConfig and runQueryConfig.get("runIndividualQueries"):
|
|
478
|
+
# so that one query doesn't fail the whole thing
|
|
479
|
+
# the only reason this isn't the default behavior is for backwards compatibility
|
|
480
|
+
query_results = []
|
|
481
|
+
for query in queries:
|
|
482
|
+
try:
|
|
483
|
+
run_query = query
|
|
484
|
+
if runQueryConfig.get("limitBy"):
|
|
485
|
+
run_query = self.apply_limit(query, runQueryConfig["limitBy"])
|
|
486
|
+
query_result = self.target_connection.query(run_query)
|
|
487
|
+
query_results.append(query_result)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
query_results.append({
|
|
490
|
+
"query": query,
|
|
491
|
+
"error": str(e),
|
|
492
|
+
})
|
|
493
|
+
results["queryResults"] = query_results
|
|
139
494
|
else:
|
|
140
495
|
if runQueryConfig and runQueryConfig.get("limitThousand"):
|
|
141
|
-
queries = [
|
|
496
|
+
queries = [
|
|
497
|
+
query.strip().rstrip(";") + " limit 1000" for query in queries
|
|
498
|
+
]
|
|
142
499
|
elif runQueryConfig and runQueryConfig.get("limitBy"):
|
|
143
|
-
queries = [
|
|
500
|
+
queries = [
|
|
501
|
+
query.strip().rstrip(";")
|
|
502
|
+
+ f" limit {runQueryConfig.get('limitBy')}"
|
|
503
|
+
for query in queries
|
|
504
|
+
]
|
|
144
505
|
query_results = [self.target_connection.query(query) for query in queries]
|
|
145
506
|
results["queryResults"] = query_results
|
|
146
507
|
if runQueryConfig and runQueryConfig.get("fieldsToRemove"):
|
|
147
|
-
results["queryResults"] = [
|
|
508
|
+
results["queryResults"] = [
|
|
509
|
+
remove_fields(query_result, runQueryConfig.get("fieldsToRemove"))
|
|
510
|
+
for query_result in results["queryResults"]
|
|
511
|
+
]
|
|
148
512
|
if runQueryConfig and runQueryConfig.get("convertDatatypes"):
|
|
149
513
|
for query_result in results["queryResults"]:
|
|
150
514
|
query_result["fields"] = [
|
|
@@ -160,9 +524,18 @@ class Quill:
|
|
|
160
524
|
]
|
|
161
525
|
|
|
162
526
|
return results
|
|
163
|
-
|
|
527
|
+
|
|
164
528
|
def post_quill(self, path, payload):
|
|
529
|
+
# Custom JSON Encoder to handle Enums
|
|
530
|
+
class EnumEncoder(json.JSONEncoder):
|
|
531
|
+
def default(self, obj):
|
|
532
|
+
if isinstance(obj, Enum):
|
|
533
|
+
return obj.value # Convert enum to its value (string in this case)
|
|
534
|
+
return super().default(obj)
|
|
535
|
+
|
|
165
536
|
url = f"{self.baseUrl}/sdk/{path}"
|
|
166
|
-
|
|
167
|
-
|
|
537
|
+
# Set content type to application/json
|
|
538
|
+
headers = {"Authorization": f"Bearer {self.private_key}", "Content-Type": "application/json"}
|
|
539
|
+
encoded = json.dumps(payload, cls=EnumEncoder)
|
|
540
|
+
response = requests.post(url, data=encoded, headers=headers)
|
|
168
541
|
return response.json()
|
quillsql/db/__init__.py
CHANGED