api-foundry-query-engine 0.0.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.
- api_foundry_query_engine/adapters/adapter.py +67 -0
- api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
- api_foundry_query_engine/adapters/gateway_adapter.py +112 -0
- api_foundry_query_engine/connectors/connection.py +32 -0
- api_foundry_query_engine/connectors/connection_factory.py +110 -0
- api_foundry_query_engine/connectors/oracle_connector.py +29 -0
- api_foundry_query_engine/connectors/postgres_connection.py +105 -0
- api_foundry_query_engine/dao/dao.py +23 -0
- api_foundry_query_engine/dao/operation_dao.py +141 -0
- api_foundry_query_engine/dao/sql_custom_query_handler.py +66 -0
- api_foundry_query_engine/dao/sql_delete_query_handler.py +36 -0
- api_foundry_query_engine/dao/sql_insert_query_handler.py +102 -0
- api_foundry_query_engine/dao/sql_query_handler.py +386 -0
- api_foundry_query_engine/dao/sql_select_query_handler.py +305 -0
- api_foundry_query_engine/dao/sql_subselect_query_handler.py +63 -0
- api_foundry_query_engine/dao/sql_update_query_handler.py +59 -0
- api_foundry_query_engine/handler.py +48 -0
- api_foundry_query_engine/operation.py +15 -0
- api_foundry_query_engine/services/service.py +60 -0
- api_foundry_query_engine/services/transactional_service.py +43 -0
- api_foundry_query_engine/utils/api_model.py +175 -0
- api_foundry_query_engine/utils/app_exception.py +22 -0
- api_foundry_query_engine/utils/logger.py +60 -0
- api_foundry_query_engine-0.0.1.dist-info/METADATA +14 -0
- api_foundry_query_engine-0.0.1.dist-info/RECORD +27 -0
- api_foundry_query_engine-0.0.1.dist-info/WHEEL +4 -0
- api_foundry_query_engine-0.0.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional, List, Dict
|
|
3
|
+
from datetime import datetime, date
|
|
4
|
+
|
|
5
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
6
|
+
from api_foundry_query_engine.operation import Operation
|
|
7
|
+
from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
|
|
8
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
9
|
+
|
|
10
|
+
log = logger(__name__)
|
|
11
|
+
|
|
12
|
+
SQL_RESERVED_WORDS = {
|
|
13
|
+
"select",
|
|
14
|
+
"from",
|
|
15
|
+
"where",
|
|
16
|
+
"insert",
|
|
17
|
+
"update",
|
|
18
|
+
"delete",
|
|
19
|
+
"join",
|
|
20
|
+
"on",
|
|
21
|
+
"order",
|
|
22
|
+
"group",
|
|
23
|
+
"having",
|
|
24
|
+
"union",
|
|
25
|
+
"distinct",
|
|
26
|
+
"into",
|
|
27
|
+
"as",
|
|
28
|
+
"and",
|
|
29
|
+
"or",
|
|
30
|
+
"not",
|
|
31
|
+
"in",
|
|
32
|
+
"is",
|
|
33
|
+
"null",
|
|
34
|
+
"like",
|
|
35
|
+
"between",
|
|
36
|
+
"by",
|
|
37
|
+
"case",
|
|
38
|
+
"when",
|
|
39
|
+
"then",
|
|
40
|
+
"else",
|
|
41
|
+
"end",
|
|
42
|
+
"exists",
|
|
43
|
+
"all",
|
|
44
|
+
"any",
|
|
45
|
+
"some",
|
|
46
|
+
"limit",
|
|
47
|
+
"offset",
|
|
48
|
+
"fetch",
|
|
49
|
+
"for",
|
|
50
|
+
"create",
|
|
51
|
+
"alter",
|
|
52
|
+
"drop",
|
|
53
|
+
"table",
|
|
54
|
+
"index",
|
|
55
|
+
"view",
|
|
56
|
+
"trigger",
|
|
57
|
+
"procedure",
|
|
58
|
+
"function",
|
|
59
|
+
"database",
|
|
60
|
+
"schema",
|
|
61
|
+
"grant",
|
|
62
|
+
"revoke",
|
|
63
|
+
"primary",
|
|
64
|
+
"key",
|
|
65
|
+
"foreign",
|
|
66
|
+
"references",
|
|
67
|
+
"check",
|
|
68
|
+
"unique",
|
|
69
|
+
"default",
|
|
70
|
+
"with",
|
|
71
|
+
"values",
|
|
72
|
+
"set",
|
|
73
|
+
"transaction",
|
|
74
|
+
"commit",
|
|
75
|
+
"rollback",
|
|
76
|
+
"savepoint",
|
|
77
|
+
"lock",
|
|
78
|
+
"tablespace",
|
|
79
|
+
"sequence",
|
|
80
|
+
"if",
|
|
81
|
+
"else",
|
|
82
|
+
"elsif",
|
|
83
|
+
"loop",
|
|
84
|
+
"begin",
|
|
85
|
+
"declare",
|
|
86
|
+
"end",
|
|
87
|
+
"open",
|
|
88
|
+
"fetch",
|
|
89
|
+
"close",
|
|
90
|
+
"cursor",
|
|
91
|
+
"next",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
RELATIONAL_TYPES = {
|
|
95
|
+
"lt": "<",
|
|
96
|
+
"le": "<=",
|
|
97
|
+
"eq": "=",
|
|
98
|
+
"ge": ">=",
|
|
99
|
+
"gt": ">",
|
|
100
|
+
"in": "in",
|
|
101
|
+
"not-in": "not-in",
|
|
102
|
+
"between": "between",
|
|
103
|
+
"not-between": "not-between",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SQLQueryHandler:
|
|
108
|
+
operation: Operation
|
|
109
|
+
engine: str
|
|
110
|
+
|
|
111
|
+
def __init__(self, operation: Operation, engine: str):
|
|
112
|
+
self.operation = operation
|
|
113
|
+
self.__select_list_columns = None
|
|
114
|
+
self.engine = engine
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def sql(self) -> str:
|
|
118
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def placeholders(self) -> Dict[str, SchemaObjectProperty]:
|
|
122
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def select_list_columns(self) -> List[str]:
|
|
126
|
+
if not self.__select_list_columns:
|
|
127
|
+
self.__select_list_columns = list(self.selection_results.keys())
|
|
128
|
+
return self.__select_list_columns
|
|
129
|
+
|
|
130
|
+
def marshal_record(self, record: dict) -> dict:
|
|
131
|
+
result = {}
|
|
132
|
+
for name, value in record.items():
|
|
133
|
+
property = self.selection_results[name]
|
|
134
|
+
result[property.api_name] = property.convert_to_api_value(value)
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
def placeholder(self, property: SchemaObjectProperty, param: str = "") -> str:
|
|
138
|
+
if len(param) == 0:
|
|
139
|
+
param = property.api_name
|
|
140
|
+
|
|
141
|
+
if self.engine == "oracle":
|
|
142
|
+
if property.column_type == "date":
|
|
143
|
+
return f"TO_DATE(:{param}, 'YYYY-MM-DD')"
|
|
144
|
+
elif property.column_type == "datetime":
|
|
145
|
+
return f"TO_TIMESTAMP(:{param}, 'YYYY-MM-DD\"T\"HH24:MI:SS.FF')"
|
|
146
|
+
elif property.column_type == "time":
|
|
147
|
+
return f"TO_TIME(:{param}, 'HH24:MI:SS.FF')"
|
|
148
|
+
return f":{param}"
|
|
149
|
+
return f"%({param})s"
|
|
150
|
+
|
|
151
|
+
def generate_sql_condition(
|
|
152
|
+
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
153
|
+
) -> str:
|
|
154
|
+
operand = "="
|
|
155
|
+
|
|
156
|
+
if isinstance(value, str):
|
|
157
|
+
parts = value.split("::", 1)
|
|
158
|
+
operand = RELATIONAL_TYPES.get(parts[0], "=") if len(parts) > 1 else "="
|
|
159
|
+
value_str = parts[-1]
|
|
160
|
+
elif isinstance(value, (datetime, date)):
|
|
161
|
+
value_str = value.isoformat()
|
|
162
|
+
else:
|
|
163
|
+
value_str = str(value)
|
|
164
|
+
|
|
165
|
+
column = f"{prefix}.{property.column_name}" if prefix else property.column_name
|
|
166
|
+
placeholder_name = (
|
|
167
|
+
f"{prefix}_{property.api_name}" if prefix else property.api_name
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if operand in ["between", "not-between"]:
|
|
171
|
+
value_set = value_str.split(",")
|
|
172
|
+
sql = f"{column} {'NOT ' if operand == 'not-between' else ''}BETWEEN {self.placeholder(property, f'{placeholder_name}_1')} AND {self.placeholder(property, f'{prefix}_{property.api_name}_2')}" # noqa E501
|
|
173
|
+
elif operand in ["in", "not-in"]:
|
|
174
|
+
value_set = value_str.split(",")
|
|
175
|
+
assignments = [
|
|
176
|
+
self.placeholder(property, f"{placeholder_name}_{index}")
|
|
177
|
+
for index, _ in enumerate(value_set)
|
|
178
|
+
]
|
|
179
|
+
sql = f"{column} {'NOT ' if operand == 'not-in' else ''}IN ({', '.join(assignments)})" # noqa E501
|
|
180
|
+
else:
|
|
181
|
+
sql = f"{column} {operand} {self.placeholder(property, placeholder_name)}"
|
|
182
|
+
return sql
|
|
183
|
+
|
|
184
|
+
def generate_placeholders(
|
|
185
|
+
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
186
|
+
) -> dict:
|
|
187
|
+
operand = "="
|
|
188
|
+
|
|
189
|
+
if isinstance(value, str):
|
|
190
|
+
parts = value.split("::", 1)
|
|
191
|
+
operand = RELATIONAL_TYPES.get(parts[0], "=") if len(parts) > 1 else "="
|
|
192
|
+
value_str = parts[-1]
|
|
193
|
+
elif isinstance(value, (datetime, date)):
|
|
194
|
+
value_str = value.isoformat()
|
|
195
|
+
else:
|
|
196
|
+
value_str = str(value)
|
|
197
|
+
|
|
198
|
+
placeholder_name = (
|
|
199
|
+
f"{prefix}_{property.api_name}" if prefix else property.api_name
|
|
200
|
+
)
|
|
201
|
+
placeholders = {}
|
|
202
|
+
|
|
203
|
+
if operand in ["between", "not-between"]:
|
|
204
|
+
value_set = value_str.split(",")
|
|
205
|
+
placeholders = {
|
|
206
|
+
f"{placeholder_name}_1": property.convert_to_db_value(value_set[0]),
|
|
207
|
+
f"{placeholder_name}_2": property.convert_to_db_value(value_set[1]),
|
|
208
|
+
}
|
|
209
|
+
elif operand in ["in", "not-in"]:
|
|
210
|
+
value_set = value_str.split(",")
|
|
211
|
+
for index, item in enumerate(value_set):
|
|
212
|
+
item_name = f"{placeholder_name}_{index}"
|
|
213
|
+
placeholders[item_name] = property.convert_to_db_value(item)
|
|
214
|
+
else:
|
|
215
|
+
placeholders = {placeholder_name: property.convert_to_db_value(value_str)}
|
|
216
|
+
|
|
217
|
+
return placeholders
|
|
218
|
+
|
|
219
|
+
def search_value_assignment(
|
|
220
|
+
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
221
|
+
) -> tuple[str, dict]:
|
|
222
|
+
sql_condition = self.generate_sql_condition(property, value, prefix)
|
|
223
|
+
placeholders = self.generate_placeholders(property, value, prefix)
|
|
224
|
+
return sql_condition, placeholders
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def selection_results(self) -> Dict:
|
|
228
|
+
if not hasattr(self, "_selection_results"):
|
|
229
|
+
self._selection_results = self.selection_result_map()
|
|
230
|
+
return self._selection_results
|
|
231
|
+
|
|
232
|
+
def selection_result_map(self) -> Dict:
|
|
233
|
+
raise NotImplementedError()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
237
|
+
schema_object: SchemaObject
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
241
|
+
) -> None:
|
|
242
|
+
super().__init__(operation, engine)
|
|
243
|
+
self.schema_object = schema_object
|
|
244
|
+
self.single_table = self.__single_table()
|
|
245
|
+
self.__select_list = None
|
|
246
|
+
self.__selection_result_map = None
|
|
247
|
+
self.search_placeholders = dict()
|
|
248
|
+
self.store_placeholders = dict()
|
|
249
|
+
self.active_prefixes = set()
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def sql(self) -> str:
|
|
253
|
+
raise NotImplementedError("Subclasses should implement this method")
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def placeholders(self) -> dict:
|
|
257
|
+
return {**self.search_placeholders, **self.store_placeholders}
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def prefix_map(self) -> Dict[str, str]:
|
|
261
|
+
if not hasattr(self, "_prefix_map"):
|
|
262
|
+
self._prefix_map = {}
|
|
263
|
+
for entity in [
|
|
264
|
+
self.schema_object.api_name,
|
|
265
|
+
*self.schema_object.relations.keys(),
|
|
266
|
+
]:
|
|
267
|
+
entity_lower = entity.lower()
|
|
268
|
+
for i in range(1, len(entity_lower) + 1):
|
|
269
|
+
substring = entity_lower[:i]
|
|
270
|
+
if (
|
|
271
|
+
substring not in self._prefix_map.values()
|
|
272
|
+
and substring not in SQL_RESERVED_WORDS
|
|
273
|
+
):
|
|
274
|
+
self._prefix_map[entity] = substring
|
|
275
|
+
break
|
|
276
|
+
return self._prefix_map
|
|
277
|
+
|
|
278
|
+
def __single_table(self) -> bool:
|
|
279
|
+
if len(self.prefix_map) == 1 or self.operation.action == "create":
|
|
280
|
+
return True
|
|
281
|
+
if ":" in self.operation.metadata_params.get("properties", ""):
|
|
282
|
+
return False
|
|
283
|
+
for param in self.operation.query_params.keys():
|
|
284
|
+
if "." in param:
|
|
285
|
+
return False
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def select_list(self) -> str:
|
|
290
|
+
if not self.__select_list:
|
|
291
|
+
self.__select_list = ", ".join(self.select_list_columns)
|
|
292
|
+
return self.__select_list
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def table_expression(self) -> str:
|
|
296
|
+
return self.schema_object.table_name
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def search_condition(self) -> str:
|
|
300
|
+
self.search_placeholders = {}
|
|
301
|
+
conditions = []
|
|
302
|
+
for name, value in self.operation.query_params.items():
|
|
303
|
+
if "." in name:
|
|
304
|
+
raise ApplicationException(
|
|
305
|
+
400, "Selection on relations is not supported"
|
|
306
|
+
)
|
|
307
|
+
property = self.schema_object.properties.get(name)
|
|
308
|
+
if not property:
|
|
309
|
+
raise ApplicationException(
|
|
310
|
+
500, f"Search condition column not found {name}"
|
|
311
|
+
)
|
|
312
|
+
if (
|
|
313
|
+
self.operation.action != "read"
|
|
314
|
+
and isinstance(value, str)
|
|
315
|
+
and re.match(
|
|
316
|
+
r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value
|
|
317
|
+
)
|
|
318
|
+
and self.schema_object.concurrency_property
|
|
319
|
+
):
|
|
320
|
+
raise ApplicationException(
|
|
321
|
+
400,
|
|
322
|
+
"Concurrency settings prohibit multi-record updates "
|
|
323
|
+
+ self.schema_object.api_name
|
|
324
|
+
+ ", property: "
|
|
325
|
+
+ property.api_name,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
assignment, holders = self.search_value_assignment(property, value)
|
|
329
|
+
conditions.append(assignment)
|
|
330
|
+
self.search_placeholders.update(holders)
|
|
331
|
+
return f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
332
|
+
|
|
333
|
+
def selection_result_map(self) -> dict:
|
|
334
|
+
if not self.__selection_result_map:
|
|
335
|
+
filters = self.operation.metadata_params.get("_properties", ".*").split()
|
|
336
|
+
self.__selection_result_map = self.filter_and_prefix_keys(
|
|
337
|
+
filters, self.schema_object.properties
|
|
338
|
+
)
|
|
339
|
+
return self.__selection_result_map
|
|
340
|
+
|
|
341
|
+
def filter_and_prefix_keys(
|
|
342
|
+
self, regex_list: List[str], properties: dict, prefix: Optional[str] = None
|
|
343
|
+
) -> dict:
|
|
344
|
+
"""
|
|
345
|
+
Accepts a prefix string, list of regular expressions, and a dictionary.
|
|
346
|
+
Returns a new dictionary containing items whose keys match any of the
|
|
347
|
+
regular expressions, with the prefix string prepended to the key
|
|
348
|
+
values of the dictionary.
|
|
349
|
+
|
|
350
|
+
Parameters:
|
|
351
|
+
- prefix (str): The prefix string to prepend to the key values.
|
|
352
|
+
- regex_list (list of str): The list of regular expression patterns
|
|
353
|
+
to match keys.
|
|
354
|
+
- properties (dict): The input properties.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
- dict: A new dictionary containing filtered items with modified key values.
|
|
358
|
+
"""
|
|
359
|
+
filtered_dict = {}
|
|
360
|
+
compiled_regexes = [re.compile(regex) for regex in regex_list]
|
|
361
|
+
for key, value in properties.items():
|
|
362
|
+
for pattern in compiled_regexes:
|
|
363
|
+
if pattern.match(key):
|
|
364
|
+
filtered_dict[f"{prefix}.{key}" if prefix else key] = value
|
|
365
|
+
self.active_prefixes.add(prefix)
|
|
366
|
+
break
|
|
367
|
+
return filtered_dict
|
|
368
|
+
|
|
369
|
+
def concurrency_generator(self, property: SchemaObjectProperty) -> str:
|
|
370
|
+
if property.api_type == "date-time":
|
|
371
|
+
return "CURRENT_TIMESTAMP"
|
|
372
|
+
elif property.api_type == "integer":
|
|
373
|
+
return f"{property.column_name} + 1"
|
|
374
|
+
elif property.api_type in ["string", "uuid"]:
|
|
375
|
+
if self.engine == "oracle":
|
|
376
|
+
return "SYS_GUID()"
|
|
377
|
+
if self.engine == "mysql":
|
|
378
|
+
return "UUID()"
|
|
379
|
+
return "gen_random_uuid()"
|
|
380
|
+
raise ApplicationException(
|
|
381
|
+
500,
|
|
382
|
+
(
|
|
383
|
+
"Concurrency control property is unrecognized type"
|
|
384
|
+
+ f"name: {property.api_name}, type: {property.api_type}"
|
|
385
|
+
),
|
|
386
|
+
)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
2
|
+
from api_foundry_query_engine.operation import Operation
|
|
3
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
4
|
+
from api_foundry_query_engine.utils.api_model import (
|
|
5
|
+
SchemaObject,
|
|
6
|
+
SchemaObjectProperty
|
|
7
|
+
)
|
|
8
|
+
from api_foundry.utils.logger import logger
|
|
9
|
+
|
|
10
|
+
log = logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
14
|
+
def __init__(
|
|
15
|
+
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(operation, schema_object, engine)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def sql(self) -> str:
|
|
21
|
+
# order is important here table_expression must be last
|
|
22
|
+
search_condition = self.search_condition
|
|
23
|
+
order_by_expression = self.order_by_expression
|
|
24
|
+
select_list = self.select_list
|
|
25
|
+
table_expression = self.table_expression
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
f"SELECT {select_list}"
|
|
29
|
+
+ f" FROM {table_expression}"
|
|
30
|
+
+ search_condition
|
|
31
|
+
+ order_by_expression
|
|
32
|
+
+ self.limit_expression
|
|
33
|
+
+ self.offset_expression
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def select_list(self) -> str:
|
|
38
|
+
if self.operation.metadata_params.get("count", False):
|
|
39
|
+
return "count(*)"
|
|
40
|
+
return super().select_list
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def search_condition(self) -> str:
|
|
44
|
+
self.search_placeholders = {}
|
|
45
|
+
conditions = []
|
|
46
|
+
|
|
47
|
+
for name, value in self.operation.query_params.items():
|
|
48
|
+
parts = name.split(".")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
if len(parts) > 1:
|
|
52
|
+
if parts[0] not in self.schema_object.relations:
|
|
53
|
+
raise ApplicationException(
|
|
54
|
+
400,
|
|
55
|
+
"Invalid selection property "
|
|
56
|
+
+ self.schema_object.api_name
|
|
57
|
+
+ " does not have a property "
|
|
58
|
+
+ parts[0],
|
|
59
|
+
)
|
|
60
|
+
relation = self.schema_object.relations[parts[0]]
|
|
61
|
+
if parts[1] not in relation.child_schema_object.properties:
|
|
62
|
+
raise ApplicationException(
|
|
63
|
+
400,
|
|
64
|
+
"Property not found, "
|
|
65
|
+
+ relation.child_schema_object.api_name
|
|
66
|
+
+ " does not have property "
|
|
67
|
+
+ parts[1],
|
|
68
|
+
)
|
|
69
|
+
property = relation.child_schema_object.properties[parts[1]]
|
|
70
|
+
prefix = self.prefix_map[parts[0]]
|
|
71
|
+
else:
|
|
72
|
+
property = self.schema_object.properties[parts[0]]
|
|
73
|
+
prefix = self.prefix_map[self.schema_object.api_name]
|
|
74
|
+
except KeyError:
|
|
75
|
+
raise ApplicationException(
|
|
76
|
+
500,
|
|
77
|
+
(
|
|
78
|
+
"Invalid query parameter, property not found. "
|
|
79
|
+
+ "schema object: "
|
|
80
|
+
+ self.schema_object.api_name
|
|
81
|
+
+ ", property: "
|
|
82
|
+
+ name
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assignment, holders = self.search_value_assignment(property, value, prefix)
|
|
87
|
+
self.active_prefixes.add(prefix)
|
|
88
|
+
conditions.append(assignment)
|
|
89
|
+
self.search_placeholders.update(holders)
|
|
90
|
+
|
|
91
|
+
return f" WHERE {' AND '.join(conditions)}" if len(conditions) > 0 else ""
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def table_expression(self) -> str:
|
|
95
|
+
joins = []
|
|
96
|
+
parent_prefix = self.prefix_map[self.schema_object.api_name]
|
|
97
|
+
for name, relation in self.schema_object.relations.items():
|
|
98
|
+
child_prefix = self.prefix_map[relation.api_name]
|
|
99
|
+
if child_prefix in self.active_prefixes:
|
|
100
|
+
joins.append(
|
|
101
|
+
"INNER JOIN "
|
|
102
|
+
+ relation.child_schema_object.table_name
|
|
103
|
+
+ " AS "
|
|
104
|
+
+ child_prefix
|
|
105
|
+
+ " ON "
|
|
106
|
+
+ parent_prefix
|
|
107
|
+
+ "."
|
|
108
|
+
+ relation.parent_property
|
|
109
|
+
+ " = "
|
|
110
|
+
+ child_prefix
|
|
111
|
+
+ "."
|
|
112
|
+
+ relation.child_property
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
self.schema_object.table_name
|
|
117
|
+
+ " AS "
|
|
118
|
+
+ self.prefix_map[self.schema_object.api_name]
|
|
119
|
+
+ (f" {' '.join(joins)}" if len(joins) > 0 else "")
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def selection_result_map(self) -> dict:
|
|
123
|
+
if "count" in self.operation.metadata_params:
|
|
124
|
+
self._selection_results = {
|
|
125
|
+
"count": SchemaObjectProperty( {
|
|
126
|
+
"api_name": "count",
|
|
127
|
+
"api_type": "integer",
|
|
128
|
+
"column_name": "count(*)",
|
|
129
|
+
"column_type": "integer"
|
|
130
|
+
} )
|
|
131
|
+
}
|
|
132
|
+
return self._selection_results
|
|
133
|
+
|
|
134
|
+
filter_str = self.operation.metadata_params.get("properties", ".*")
|
|
135
|
+
self._selection_results = {}
|
|
136
|
+
|
|
137
|
+
for relation, reg_exs in self.get_regex_map(filter_str).items():
|
|
138
|
+
# Extract the schema object for the current entity
|
|
139
|
+
relation_property = self.schema_object.relations.get(relation)
|
|
140
|
+
|
|
141
|
+
if relation_property:
|
|
142
|
+
if relation_property.type == "array":
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Use a default value if relation_property is None
|
|
146
|
+
schema_object = relation_property.child_schema_object
|
|
147
|
+
else:
|
|
148
|
+
schema_object = self.schema_object
|
|
149
|
+
|
|
150
|
+
if relation not in self.prefix_map:
|
|
151
|
+
raise ApplicationException(
|
|
152
|
+
400,
|
|
153
|
+
"Bad object association: "
|
|
154
|
+
+ schema_object.api_name
|
|
155
|
+
+ " does not have a "
|
|
156
|
+
+ relation
|
|
157
|
+
+ " property",
|
|
158
|
+
)
|
|
159
|
+
# Filter and prefix keys for the current entity
|
|
160
|
+
# and regular expressions
|
|
161
|
+
filtered_keys = self.filter_and_prefix_keys(
|
|
162
|
+
reg_exs, schema_object.properties, self.prefix_map[relation]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Extend the result map with the filtered keys
|
|
166
|
+
self._selection_results.update(filtered_keys)
|
|
167
|
+
|
|
168
|
+
return self._selection_results
|
|
169
|
+
|
|
170
|
+
def get_regex_map(self, filter_str: str) -> dict[str, list]:
|
|
171
|
+
result = {}
|
|
172
|
+
|
|
173
|
+
for filter in filter_str.split():
|
|
174
|
+
parts = filter.split(":")
|
|
175
|
+
entity = parts[0] if len(parts) > 1 else self.schema_object.api_name
|
|
176
|
+
expression = parts[-1]
|
|
177
|
+
|
|
178
|
+
# Check if entity already exists in result, if not, initialize
|
|
179
|
+
# it with an empty list
|
|
180
|
+
if entity not in result:
|
|
181
|
+
result[entity] = []
|
|
182
|
+
|
|
183
|
+
# Append the expression to the list of expressions for the entity
|
|
184
|
+
result[entity].append(expression)
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
def marshal_record(self, record) -> dict:
|
|
189
|
+
object_set = {}
|
|
190
|
+
for name, value in record.items():
|
|
191
|
+
property = self.selection_results[name]
|
|
192
|
+
parts = name.split(".")
|
|
193
|
+
component = parts[0] if len(parts) > 1 else self.prefix_map[self.schema_object.api_name]
|
|
194
|
+
object = object_set.get(component, {})
|
|
195
|
+
if not object:
|
|
196
|
+
object_set[component] = object
|
|
197
|
+
object[property.api_name] = property.convert_to_api_value(value)
|
|
198
|
+
|
|
199
|
+
result = object_set[self.prefix_map[self.schema_object.api_name]]
|
|
200
|
+
for name, prefix in self.prefix_map.items():
|
|
201
|
+
if name != self.schema_object.api_name and prefix in object_set:
|
|
202
|
+
result[name] = object_set[prefix]
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def order_by_expression(self) -> str:
|
|
208
|
+
fields_str = self.operation.metadata_params.get("sort", None)
|
|
209
|
+
if not fields_str:
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
# determine the columns requested
|
|
213
|
+
fields = fields_str.replace(",", " ").split()
|
|
214
|
+
|
|
215
|
+
order_set = []
|
|
216
|
+
use_prefixes = False
|
|
217
|
+
for field in fields:
|
|
218
|
+
# handle order
|
|
219
|
+
field_parts = field.split(":")
|
|
220
|
+
field_name = field_parts[0]
|
|
221
|
+
|
|
222
|
+
order = "asc" if len(field_parts) == 1 else field_parts[1]
|
|
223
|
+
if order != "desc" and order != "asc":
|
|
224
|
+
raise ApplicationException(400, f"unrecognized sorting order: {field}")
|
|
225
|
+
|
|
226
|
+
# handle entity prefix
|
|
227
|
+
field_parts = field_name.split(".")
|
|
228
|
+
if len(field_parts) == 1:
|
|
229
|
+
prefix = self.prefix_map[self.schema_object.api_name]
|
|
230
|
+
property = self.schema_object.properties.get(field_parts[0])
|
|
231
|
+
if not property:
|
|
232
|
+
raise ApplicationException(
|
|
233
|
+
400,
|
|
234
|
+
f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
|
|
235
|
+
)
|
|
236
|
+
column = property.column_name
|
|
237
|
+
else:
|
|
238
|
+
# Extract the schema object for the current entity
|
|
239
|
+
relation_property = self.schema_object.relations.get(field_parts[0])
|
|
240
|
+
if not relation_property:
|
|
241
|
+
raise ApplicationException(
|
|
242
|
+
400,
|
|
243
|
+
f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if relation_property:
|
|
247
|
+
if relation_property.type == "array":
|
|
248
|
+
raise ApplicationException(
|
|
249
|
+
400,
|
|
250
|
+
f"Invalid order by array property is not supported, schema object: {self.schema_object.api_name} property: {field_parts[0]}", # noqa E501
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Use a default value if relation_property is None
|
|
254
|
+
schema_object = relation_property.child_schema_object
|
|
255
|
+
else:
|
|
256
|
+
schema_object = self.schema_object
|
|
257
|
+
|
|
258
|
+
prefix = self.prefix_map[field_parts[0]]
|
|
259
|
+
property = schema_object.properties.get(field_parts[1])
|
|
260
|
+
if not property:
|
|
261
|
+
raise ApplicationException(
|
|
262
|
+
400,
|
|
263
|
+
f"Invalid order by property, schema object: {schema_object.api_name} does not have a property: {field_parts[1]}", # noqa E501
|
|
264
|
+
)
|
|
265
|
+
column = property.column_name
|
|
266
|
+
self.active_prefixes.add(prefix)
|
|
267
|
+
use_prefixes = True
|
|
268
|
+
|
|
269
|
+
order_set.append((prefix, column, order))
|
|
270
|
+
|
|
271
|
+
if len(order_set) == 0:
|
|
272
|
+
return ""
|
|
273
|
+
order_parts = []
|
|
274
|
+
for prefix, column, order in order_set:
|
|
275
|
+
if use_prefixes:
|
|
276
|
+
order_parts.append(f"{prefix}.{column} {order}")
|
|
277
|
+
else:
|
|
278
|
+
order_parts.append(f"{column} {order}")
|
|
279
|
+
return " ORDER BY " + ", ".join(order_parts)
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def limit_expression(self) -> str:
|
|
283
|
+
limit_str = self.operation.metadata_params.get("limit", None)
|
|
284
|
+
if not limit_str:
|
|
285
|
+
return ""
|
|
286
|
+
|
|
287
|
+
if isinstance(limit_str, str) and not limit_str.isdigit():
|
|
288
|
+
raise ApplicationException(
|
|
289
|
+
400, f"Limit is not an valid integer {limit_str}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return f" LIMIT {limit_str}"
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def offset_expression(self) -> str:
|
|
296
|
+
offset_str = self.operation.metadata_params.get("offset", None)
|
|
297
|
+
if not offset_str:
|
|
298
|
+
return ""
|
|
299
|
+
|
|
300
|
+
if isinstance(offset_str, str) and not offset_str.isdigit():
|
|
301
|
+
raise ApplicationException(
|
|
302
|
+
400, f"Offset is not an valid integer {offset_str}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return f" offset {offset_str}"
|