api-foundry-query-engine 0.8.39__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.
Files changed (37) hide show
  1. api_foundry_query_engine/.pre-commit-config.yaml +22 -0
  2. api_foundry_query_engine/__init__.py +1 -0
  3. api_foundry_query_engine/adapters/adapter.py +73 -0
  4. api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
  5. api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
  6. api_foundry_query_engine/adapters/security_adapter.py +106 -0
  7. api_foundry_query_engine/connectors/connection.py +32 -0
  8. api_foundry_query_engine/connectors/connection_factory.py +115 -0
  9. api_foundry_query_engine/connectors/oracle_connector.py +29 -0
  10. api_foundry_query_engine/connectors/postgres_connection.py +142 -0
  11. api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
  12. api_foundry_query_engine/dao/dao.py +23 -0
  13. api_foundry_query_engine/dao/operation_dao.py +186 -0
  14. api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
  15. api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
  16. api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
  17. api_foundry_query_engine/dao/sql_query_handler.py +713 -0
  18. api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
  19. api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
  20. api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
  21. api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
  22. api_foundry_query_engine/lambda_handler.py +63 -0
  23. api_foundry_query_engine/operation.py +104 -0
  24. api_foundry_query_engine/services/service.py +75 -0
  25. api_foundry_query_engine/services/transactional_service.py +52 -0
  26. api_foundry_query_engine/utils/api_model.py +380 -0
  27. api_foundry_query_engine/utils/app_exception.py +22 -0
  28. api_foundry_query_engine/utils/claims_check.py +471 -0
  29. api_foundry_query_engine/utils/dependency_resolver.py +157 -0
  30. api_foundry_query_engine/utils/gateway_operation.py +279 -0
  31. api_foundry_query_engine/utils/logger.py +60 -0
  32. api_foundry_query_engine/utils/reference_resolver.py +222 -0
  33. api_foundry_query_engine/utils/token_decoder.py +624 -0
  34. api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
  35. api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
  36. api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
  37. api_foundry_query_engine-0.8.39.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,195 @@
1
+ from api_foundry_query_engine.dao.sql_query_handler import (
2
+ SQLSchemaQueryHandler,
3
+ )
4
+ from api_foundry_query_engine.operation import Operation
5
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
6
+ from api_foundry_query_engine.utils.api_model import SchemaObject
7
+ from api_foundry_query_engine.utils.logger import logger
8
+
9
+ log = logger(__name__)
10
+
11
+
12
+ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
13
+ """Handler for restoring soft-deleted records."""
14
+
15
+ def __init__(
16
+ self, operation: Operation, schema_object: SchemaObject, engine: str
17
+ ) -> None:
18
+ super().__init__(operation, schema_object, engine)
19
+
20
+ def check_permission(self) -> bool:
21
+ """Check if user has permission to restore records."""
22
+ # if permissions are not defined then no restrictions are applied
23
+ if not self.schema_object.permissions:
24
+ return True
25
+
26
+ # Use the proper provider-action-role structure
27
+ provider_permissions = self.schema_object.permissions.get("default", {})
28
+
29
+ # Check for explicit restore permissions first
30
+ restore_permissions = provider_permissions.get("restore", {})
31
+ write_permissions = provider_permissions.get("write", {})
32
+
33
+ for role in self.operation.roles:
34
+ # Try restore permissions first
35
+ role_permissions = restore_permissions.get(role)
36
+ log.info("role: %s, restore_permissions: %s", role, role_permissions)
37
+
38
+ # If not found, fall back to write permissions
39
+ if role_permissions is None:
40
+ role_permissions = write_permissions.get(role)
41
+ log.info("Fallback to write permissions: %s", role_permissions)
42
+
43
+ # If still not found, check wildcard "*"
44
+ if role_permissions is None:
45
+ role_permissions = restore_permissions.get(
46
+ "*"
47
+ ) or write_permissions.get("*")
48
+ log.info("Fallback to wildcard role '*': %s", role_permissions)
49
+ if role_permissions is None:
50
+ continue
51
+
52
+ # Handle both boolean and object permission formats
53
+ if isinstance(role_permissions, bool):
54
+ allowed = role_permissions
55
+ else:
56
+ # For complex permission objects, existence means allowed
57
+ allowed = True
58
+
59
+ if allowed:
60
+ return True
61
+
62
+ return False
63
+
64
+ def _get_restore_update_values(self) -> str:
65
+ """Generate SET clause for restore operation."""
66
+ self.store_placeholders = {}
67
+ columns = []
68
+
69
+ # Process soft delete properties
70
+ soft_delete_props = self.schema_object.get_soft_delete_properties()
71
+
72
+ for _, prop in soft_delete_props.items():
73
+ strategy = prop.get_soft_delete_strategy()
74
+ config = prop.get_soft_delete_config()
75
+ column_name = prop.column_name
76
+
77
+ if strategy == "null_check":
78
+ columns.append(f"{column_name} = NULL")
79
+ elif strategy == "boolean_flag":
80
+ active_value = config.get("active_value", True)
81
+ columns.append(f"{column_name} = {str(active_value).lower()}")
82
+ elif strategy == "exclude_values":
83
+ restore_value = config.get("restore_value")
84
+ if restore_value:
85
+ if isinstance(restore_value, str):
86
+ columns.append(f"{column_name} = '{restore_value}'")
87
+ else:
88
+ columns.append(f"{column_name} = {restore_value}")
89
+
90
+ # Process audit fields for restore action
91
+ audit_props = self.schema_object.get_soft_delete_audit_properties()
92
+ claims = self.operation.claims or {}
93
+
94
+ for _, prop in audit_props.items():
95
+ config = prop.get_soft_delete_config()
96
+ action = config.get("action", "")
97
+
98
+ if action == "restore" and "sub" in claims:
99
+ placeholder_key = f"audit_{prop.api_name}"
100
+ columns.append(f"{prop.column_name} = %({placeholder_key})s")
101
+ self.store_placeholders[placeholder_key] = claims["sub"]
102
+ elif action == "restore_timestamp":
103
+ columns.append(f"{prop.column_name} = CURRENT_TIMESTAMP")
104
+
105
+ return " SET " + ", ".join(columns) if columns else ""
106
+
107
+ @property
108
+ def search_condition(self) -> str:
109
+ """Override to include soft-deleted records in restore search."""
110
+ self.search_placeholders = {}
111
+ conditions = []
112
+
113
+ # Don't apply soft delete filtering for restore operations
114
+ # We want to find the soft-deleted records to restore them
115
+
116
+ for name, value in self.operation.query_params.items():
117
+ if "." in name:
118
+ raise ApplicationException(
119
+ 400, "Selection on relations is not supported"
120
+ )
121
+ prop = self.schema_object.properties.get(name)
122
+ if not prop:
123
+ raise ApplicationException(
124
+ 500, f"Search condition column not found {name}"
125
+ )
126
+
127
+ assignment, holders = self.search_value_assignment(prop, value)
128
+ conditions.append(assignment)
129
+ self.search_placeholders.update(holders)
130
+
131
+ # Add condition to only restore soft-deleted records
132
+ soft_delete_conditions = self._get_soft_delete_restore_conditions()
133
+ if soft_delete_conditions:
134
+ conditions.append(f"({soft_delete_conditions})")
135
+
136
+ return f" WHERE {' AND '.join(conditions)}" if conditions else ""
137
+
138
+ def _get_soft_delete_restore_conditions(self) -> str:
139
+ """Generate conditions to identify soft-deleted records for restore."""
140
+ conditions = []
141
+
142
+ soft_delete_props = self.schema_object.get_soft_delete_properties()
143
+
144
+ for _, prop in soft_delete_props.items():
145
+ strategy = prop.get_soft_delete_strategy()
146
+ config = prop.get_soft_delete_config()
147
+ column_name = prop.column_name
148
+
149
+ if strategy == "null_check":
150
+ conditions.append(f"{column_name} IS NOT NULL")
151
+ elif strategy == "boolean_flag":
152
+ active_value = config.get("active_value", True)
153
+ inactive_value = not active_value
154
+ value_str = str(inactive_value).lower()
155
+ conditions.append(f"{column_name} = {value_str}")
156
+ elif strategy == "exclude_values":
157
+ excluded_values = config.get("values", [])
158
+ if excluded_values:
159
+ # For restore, we want records that ARE in the excluded values
160
+ formatted_values = ", ".join(
161
+ f"'{val}'" if isinstance(val, str) else str(val)
162
+ for val in excluded_values
163
+ )
164
+ conditions.append(f"{column_name} IN ({formatted_values})")
165
+
166
+ # For multiple soft delete fields, use AND logic
167
+ # A record is considered soft-deleted if ALL soft delete conditions are true
168
+ return " AND ".join(conditions) if conditions else ""
169
+
170
+ @property
171
+ def sql(self) -> str:
172
+ if not self.check_permission():
173
+ raise ApplicationException(
174
+ 403,
175
+ f"Subject is not allowed to restore "
176
+ f"{self.schema_object.api_name}",
177
+ )
178
+
179
+ if not self.schema_object.has_soft_delete_support():
180
+ raise ApplicationException(
181
+ 400,
182
+ f"Schema object {self.schema_object.api_name} does not support "
183
+ + "soft delete operations",
184
+ )
185
+
186
+ update_clause = self._get_restore_update_values()
187
+ if not update_clause:
188
+ raise ApplicationException(
189
+ 400, f"No restore fields available for {self.schema_object.api_name}"
190
+ )
191
+
192
+ return (
193
+ f"UPDATE {self.table_expression}{update_clause}"
194
+ + f"{self.search_condition} RETURNING {self.select_list}"
195
+ )
@@ -0,0 +1,433 @@
1
+ import re
2
+ from typing import Match
3
+ from api_foundry_query_engine.dao.sql_query_handler import (
4
+ SQLSchemaQueryHandler,
5
+ )
6
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
7
+ from api_foundry_query_engine.utils.api_model import SchemaObjectProperty
8
+
9
+
10
+ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
11
+ def _soft_delete_where_clause(self) -> str:
12
+ """
13
+ Generate WHERE clause to filter out soft-deleted records.
14
+
15
+ Uses smart conflict detection - if query explicitly requests
16
+ soft-deleted values, those filters are skipped to allow access.
17
+ Adds table prefixes for JOIN queries.
18
+ """
19
+ prefix = self.prefix_map[str(self.schema_object.api_name)]
20
+ conditions = []
21
+
22
+ soft_delete_props = self.schema_object.get_soft_delete_properties()
23
+ conflicts = self._has_soft_delete_conflicts()
24
+
25
+ for prop_name, prop in soft_delete_props.items():
26
+ # Skip filtering if user explicitly queries for soft-deleted values
27
+ if conflicts.get(prop_name, False):
28
+ continue
29
+
30
+ strategy = prop.get_soft_delete_strategy()
31
+ config = prop.get_soft_delete_config()
32
+ column_name = prop.column_name
33
+
34
+ if strategy == "null_check":
35
+ conditions.append(f"{prefix}.{column_name} IS NULL")
36
+ elif strategy == "boolean_flag":
37
+ active_value = config.get("active_value", True)
38
+ conditions.append(f"{prefix}.{column_name} = {active_value}")
39
+ elif strategy == "exclude_values":
40
+ excluded_values = config.get("values", [])
41
+ if excluded_values:
42
+ # Format values for SQL IN clause
43
+ formatted_values = ", ".join(
44
+ f"'{val}'" if isinstance(val, str) else str(val)
45
+ for val in excluded_values
46
+ )
47
+ conditions.append(
48
+ f"{prefix}.{column_name} NOT IN ({formatted_values})"
49
+ )
50
+
51
+ return " AND ".join(conditions) if conditions else ""
52
+
53
+ def _template_where(self, expr: str) -> str:
54
+ if not expr:
55
+ return expr
56
+
57
+ def _quote(val: object) -> str:
58
+ if val is None:
59
+ return "NULL"
60
+ if isinstance(val, (int, float)):
61
+ return str(val)
62
+ s = str(val).replace("'", "''")
63
+ return f"'{s}'"
64
+
65
+ def _replace(m: Match[str]) -> str:
66
+ key = m.group(1)
67
+ claims = self.operation.claims or {}
68
+ return _quote(claims.get(key))
69
+
70
+ return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
71
+
72
+ def _row_where_clause(self) -> str:
73
+ perms = getattr(self.schema_object, "permissions", None) or {}
74
+ # provider-first default
75
+ if "default" in perms:
76
+ provider = perms.get("default", {}) or {}
77
+ read_map = provider.get("read", {}) or {}
78
+ role_permissions = perms.get("default", {})
79
+ else:
80
+ # legacy role-first -> synthesize read map
81
+ read_map = {}
82
+ role_permissions = perms
83
+ for role, role_perms in perms.items():
84
+ if isinstance(role_perms, dict):
85
+ read_map[role] = role_perms.get("read")
86
+
87
+ filters = []
88
+ for role in self.operation.roles or []:
89
+ # Check for role-level WHERE clause (hybrid approach)
90
+ role_where = None
91
+ if isinstance(role_permissions.get(role), dict):
92
+ role_where = role_permissions[role].get("where")
93
+
94
+ # Check for operation-level WHERE clause
95
+ operation_where = None
96
+ rule = read_map.get(role)
97
+ if isinstance(rule, dict):
98
+ operation_where = rule.get("where")
99
+
100
+ # Operation-level takes precedence, fallback to role-level
101
+ where_clause = operation_where if operation_where else role_where
102
+
103
+ if isinstance(where_clause, str) and where_clause.strip():
104
+ filters.append(self._template_where(where_clause))
105
+
106
+ if not filters:
107
+ return ""
108
+ return "(" + ") OR (".join(filters) + ")"
109
+
110
+ def __init__(self, operation, schema_object, engine: str) -> None:
111
+ super().__init__(operation, schema_object, engine)
112
+ # Lazy cache for selection results
113
+ self._selection_results = None
114
+
115
+ @property
116
+ def sql(self) -> str:
117
+ # order is important here table_expression must be last
118
+ search_condition = self.search_condition
119
+ order_by_expression = self.order_by_expression
120
+ select_list = self.select_list
121
+ table_expression = self.table_expression
122
+
123
+ return (
124
+ f"SELECT {select_list}"
125
+ + f" FROM {table_expression}"
126
+ + search_condition
127
+ + order_by_expression
128
+ + self.limit_expression
129
+ + self.offset_expression
130
+ )
131
+
132
+ @property
133
+ def select_list(self) -> str:
134
+ if self.operation.metadata_params.get("count", False):
135
+ return "count(*)"
136
+ return super().select_list
137
+
138
+ @property
139
+ def search_condition(self) -> str:
140
+ self.search_placeholders = {}
141
+ conditions = []
142
+
143
+ # Add soft delete filtering first
144
+ soft_delete_filter = self._soft_delete_where_clause()
145
+ if soft_delete_filter:
146
+ conditions.append(soft_delete_filter)
147
+
148
+ for name, value in self.operation.query_params.items():
149
+ parts = name.split(".")
150
+
151
+ try:
152
+ if len(parts) > 1:
153
+ if parts[0] not in self.schema_object.relations:
154
+ raise ApplicationException(
155
+ 400,
156
+ "Invalid selection property "
157
+ + str(self.schema_object.api_name)
158
+ + " does not have a property "
159
+ + parts[0],
160
+ )
161
+ relation = self.schema_object.relations[parts[0]]
162
+ if parts[1] not in relation.child_schema_object.properties:
163
+ raise ApplicationException(
164
+ 400,
165
+ "Property not found, "
166
+ + str(relation.child_schema_object.api_name)
167
+ + " does not have property "
168
+ + parts[1],
169
+ )
170
+ prop = relation.child_schema_object.properties[parts[1]]
171
+ prefix = self.prefix_map[parts[0]]
172
+ else:
173
+ prop = self.schema_object.properties[parts[0]]
174
+ prefix = self.prefix_map[str(self.schema_object.api_name)]
175
+ except KeyError as exc:
176
+ raise ApplicationException(
177
+ 500,
178
+ (
179
+ "Invalid query parameter, property not found. "
180
+ + "schema object: "
181
+ + str(self.schema_object.api_name)
182
+ + ", property: "
183
+ + name
184
+ ),
185
+ ) from exc
186
+
187
+ assignment, holders = self.search_value_assignment(prop, value, prefix)
188
+ self.active_prefixes.add(prefix)
189
+ conditions.append(assignment)
190
+ self.search_placeholders.update(holders)
191
+ # append row-level filters if present
192
+ row_filter = self._row_where_clause()
193
+ if row_filter:
194
+ conditions.append(f"({row_filter})")
195
+
196
+ return (" WHERE " + " AND ".join(conditions)) if conditions else ""
197
+
198
+ @property
199
+ def table_expression(self) -> str:
200
+ joins = []
201
+ parent_prefix = self.prefix_map[str(self.schema_object.api_name)]
202
+ for _, relation in self.schema_object.relations.items():
203
+ child_prefix = self.prefix_map[str(relation.api_name)]
204
+ if child_prefix in self.active_prefixes:
205
+ joins.append(
206
+ "INNER JOIN "
207
+ + str(relation.child_schema_object.qualified_name)
208
+ + " AS "
209
+ + child_prefix
210
+ + " ON "
211
+ + parent_prefix
212
+ + "."
213
+ + relation.parent_property
214
+ + " = "
215
+ + child_prefix
216
+ + "."
217
+ + relation.child_property
218
+ )
219
+
220
+ return (
221
+ str(self.schema_object.qualified_name)
222
+ + " AS "
223
+ + str(self.prefix_map[str(self.schema_object.api_name)])
224
+ + (f" {' '.join(joins)}" if len(joins) > 0 else "")
225
+ )
226
+
227
+ @property
228
+ def selection_results(self) -> dict:
229
+ if self._selection_results is None:
230
+ self._selection_results = {}
231
+ if "count" in self.operation.metadata_params:
232
+ self._selection_results = {
233
+ "count": SchemaObjectProperty(
234
+ {
235
+ "api_name": "count",
236
+ "api_type": "integer",
237
+ "column_name": "count(*)",
238
+ "column_type": "integer",
239
+ }
240
+ )
241
+ }
242
+ return self._selection_results
243
+
244
+ filter_str = self.operation.metadata_params.get("properties", ".*")
245
+
246
+ for relation, reg_exs in self.get_regex_map(filter_str).items():
247
+ # Extract the schema object for the current entity
248
+ relation_property = self.schema_object.relations.get(relation)
249
+
250
+ if relation_property:
251
+ if relation_property.type == "array":
252
+ continue
253
+
254
+ # Use a default value if relation_property is None
255
+ schema_object = relation_property.child_schema_object
256
+ else:
257
+ schema_object = self.schema_object
258
+
259
+ if relation not in self.prefix_map:
260
+ raise ApplicationException(
261
+ 400,
262
+ "Bad object association: "
263
+ + str(schema_object.api_name)
264
+ + " does not have a "
265
+ + relation
266
+ + " property",
267
+ )
268
+ # Filter and prefix keys for the current entity
269
+ # and regular expressions
270
+ allowed_properties = self.check_permissions(
271
+ "read",
272
+ schema_object.permissions,
273
+ schema_object.properties,
274
+ )
275
+ filtered_keys = self.filter_and_prefix_keys(
276
+ reg_exs,
277
+ allowed_properties,
278
+ self.prefix_map[relation],
279
+ )
280
+
281
+ # Extend the result map with the filtered keys
282
+ self._selection_results.update(filtered_keys)
283
+
284
+ if len(self._selection_results) == 0:
285
+ raise ApplicationException(
286
+ 403,
287
+ (
288
+ "After applying permissions there are no properties "
289
+ "returned in response"
290
+ ),
291
+ )
292
+ return self._selection_results
293
+
294
+ def get_regex_map(self, filter_str: str) -> dict:
295
+ result = {}
296
+
297
+ for flt in filter_str.split():
298
+ parts = flt.split(":")
299
+ entity = parts[0] if len(parts) > 1 else self.schema_object.api_name
300
+ expression = parts[-1]
301
+
302
+ # Check if entity already exists in result, if not, initialize
303
+ # it with an empty list
304
+ if entity not in result:
305
+ result[entity] = []
306
+
307
+ # Append the expression to the list of expressions for the entity
308
+ result[entity].append(expression)
309
+
310
+ return result
311
+
312
+ def marshal_record(self, record) -> dict:
313
+ object_set = {}
314
+ for name, value in record.items():
315
+ prop = self.selection_results[name]
316
+ parts = name.split(".")
317
+ component = (
318
+ parts[0]
319
+ if len(parts) > 1
320
+ else self.prefix_map[str(self.schema_object.api_name)]
321
+ )
322
+ obj = object_set.get(component, {})
323
+ if not obj:
324
+ object_set[component] = obj
325
+ obj[prop.api_name] = prop.convert_to_api_value(value)
326
+
327
+ result = object_set[self.prefix_map[str(self.schema_object.api_name)]]
328
+ for name, prefix in self.prefix_map.items():
329
+ if name != self.schema_object.api_name and prefix in object_set:
330
+ result[name] = object_set[prefix]
331
+
332
+ return result
333
+
334
+ @property
335
+ def order_by_expression(self) -> str:
336
+ fields_str = self.operation.metadata_params.get("sort", None)
337
+ if not fields_str:
338
+ return ""
339
+
340
+ # determine the columns requested
341
+ fields = fields_str.replace(",", " ").split()
342
+
343
+ order_set = []
344
+ use_prefixes = False
345
+ for field in fields:
346
+ # handle order
347
+ field_parts = field.split(":")
348
+ field_name = field_parts[0]
349
+
350
+ order = "asc" if len(field_parts) == 1 else field_parts[1]
351
+ if order != "desc" and order != "asc":
352
+ raise ApplicationException(400, f"unrecognized sorting order: {field}")
353
+
354
+ # handle entity prefix
355
+ field_parts = field_name.split(".")
356
+ if len(field_parts) == 1:
357
+ prefix = self.prefix_map[str(self.schema_object.api_name)]
358
+ prop = self.schema_object.properties.get(field_parts[0])
359
+ if not prop:
360
+ raise ApplicationException(
361
+ 400,
362
+ f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
363
+ )
364
+ column = prop.column_name
365
+ else:
366
+ # Extract the schema object for the current entity
367
+ relation_property = self.schema_object.relations.get(field_parts[0])
368
+ if not relation_property:
369
+ raise ApplicationException(
370
+ 400,
371
+ f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
372
+ )
373
+
374
+ if relation_property:
375
+ if relation_property.type == "array":
376
+ raise ApplicationException(
377
+ 400,
378
+ f"Invalid order by array property is not supported, schema object: {self.schema_object.api_name} property: {field_parts[0]}", # noqa E501
379
+ )
380
+
381
+ # Use a default value if relation_property is None
382
+ schema_object = relation_property.child_schema_object
383
+ else:
384
+ schema_object = self.schema_object
385
+
386
+ prefix = self.prefix_map[field_parts[0]]
387
+ prop = schema_object.properties.get(field_parts[1])
388
+ if not prop:
389
+ raise ApplicationException(
390
+ 400,
391
+ f"Invalid order by property, schema object: {schema_object.api_name} does not have a property: {field_parts[1]}", # noqa E501
392
+ )
393
+ column = prop.column_name
394
+ self.active_prefixes.add(prefix)
395
+ use_prefixes = True
396
+
397
+ order_set.append((prefix, column, order))
398
+
399
+ if len(order_set) == 0:
400
+ return ""
401
+ order_parts = []
402
+ for prefix, column, order in order_set:
403
+ if use_prefixes:
404
+ order_parts.append(f"{prefix}.{column} {order}")
405
+ else:
406
+ order_parts.append(f"{column} {order}")
407
+ return " ORDER BY " + ", ".join(order_parts)
408
+
409
+ @property
410
+ def limit_expression(self) -> str:
411
+ limit_str = self.operation.metadata_params.get("limit", None)
412
+ if not limit_str:
413
+ return ""
414
+
415
+ if isinstance(limit_str, str) and not limit_str.isdigit():
416
+ raise ApplicationException(
417
+ 400, f"Limit is not an valid integer {limit_str}"
418
+ )
419
+
420
+ return f" LIMIT {limit_str}"
421
+
422
+ @property
423
+ def offset_expression(self) -> str:
424
+ offset_str = self.operation.metadata_params.get("offset", None)
425
+ if not offset_str:
426
+ return ""
427
+
428
+ if isinstance(offset_str, str) and not offset_str.isdigit():
429
+ raise ApplicationException(
430
+ 400, f"Offset is not an valid integer {offset_str}"
431
+ )
432
+
433
+ return f" offset {offset_str}"
@@ -0,0 +1,66 @@
1
+ from typing import Optional
2
+
3
+ from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
4
+ from api_foundry_query_engine.dao.sql_select_query_handler import (
5
+ SQLSelectSchemaQueryHandler,
6
+ )
7
+ from api_foundry_query_engine.operation import Operation
8
+ from api_foundry_query_engine.utils.api_model import SchemaObjectAssociation
9
+
10
+
11
+ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
12
+ def __init__(
13
+ self,
14
+ operation: Operation,
15
+ relation: SchemaObjectAssociation,
16
+ parent_generator: SQLSchemaQueryHandler,
17
+ ) -> None:
18
+ super().__init__(
19
+ operation, relation.child_schema_object, parent_generator.engine
20
+ )
21
+ self.relation = relation
22
+ self.parent_generator = parent_generator
23
+
24
+ @property
25
+ def selection_results(self) -> dict:
26
+ filter_str = self.operation.metadata_params.get("properties", ".*")
27
+ result = {self.relation.child_property: self.relation.child_property}
28
+
29
+ for relation_name, reg_exs in self.get_regex_map(filter_str).items():
30
+ if relation_name != self.relation.api_name:
31
+ continue
32
+
33
+ schema_object = self.relation.child_schema_object
34
+
35
+ # Filter and prefix keys for the current entity and regular expressions
36
+ filtered_keys = self.filter_and_prefix_keys(
37
+ reg_exs, schema_object.properties
38
+ )
39
+
40
+ # Extend the result map with the filtered keys
41
+ result.update(filtered_keys)
42
+
43
+ return result
44
+
45
+ @property
46
+ def placeholders(self) -> dict:
47
+ return self.search_placeholders
48
+
49
+ @property
50
+ def sql(self) -> Optional[str]:
51
+ if len(self.select_list_columns) == 1: # then it only contains the key
52
+ return None
53
+
54
+ sql = (
55
+ f"SELECT {self.select_list} "
56
+ + f"FROM {self.relation.child_schema_object.qualified_name} "
57
+ + f"WHERE {self.relation.child_property} "
58
+ + f"IN ( SELECT {self.relation.parent_property} "
59
+ + f"FROM {self.parent_generator.table_expression}"
60
+ + f"{self.parent_generator.search_condition} "
61
+ # + f"{order_by} {limit} {offset})"
62
+ + ")"
63
+ )
64
+ self.search_placeholders = self.parent_generator.search_placeholders
65
+ # self._execute_sql(args["cursor"], sql, query_parameters)
66
+ return sql