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.
Files changed (27) hide show
  1. api_foundry_query_engine/adapters/adapter.py +67 -0
  2. api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
  3. api_foundry_query_engine/adapters/gateway_adapter.py +112 -0
  4. api_foundry_query_engine/connectors/connection.py +32 -0
  5. api_foundry_query_engine/connectors/connection_factory.py +110 -0
  6. api_foundry_query_engine/connectors/oracle_connector.py +29 -0
  7. api_foundry_query_engine/connectors/postgres_connection.py +105 -0
  8. api_foundry_query_engine/dao/dao.py +23 -0
  9. api_foundry_query_engine/dao/operation_dao.py +141 -0
  10. api_foundry_query_engine/dao/sql_custom_query_handler.py +66 -0
  11. api_foundry_query_engine/dao/sql_delete_query_handler.py +36 -0
  12. api_foundry_query_engine/dao/sql_insert_query_handler.py +102 -0
  13. api_foundry_query_engine/dao/sql_query_handler.py +386 -0
  14. api_foundry_query_engine/dao/sql_select_query_handler.py +305 -0
  15. api_foundry_query_engine/dao/sql_subselect_query_handler.py +63 -0
  16. api_foundry_query_engine/dao/sql_update_query_handler.py +59 -0
  17. api_foundry_query_engine/handler.py +48 -0
  18. api_foundry_query_engine/operation.py +15 -0
  19. api_foundry_query_engine/services/service.py +60 -0
  20. api_foundry_query_engine/services/transactional_service.py +43 -0
  21. api_foundry_query_engine/utils/api_model.py +175 -0
  22. api_foundry_query_engine/utils/app_exception.py +22 -0
  23. api_foundry_query_engine/utils/logger.py +60 -0
  24. api_foundry_query_engine-0.0.1.dist-info/METADATA +14 -0
  25. api_foundry_query_engine-0.0.1.dist-info/RECORD +27 -0
  26. api_foundry_query_engine-0.0.1.dist-info/WHEEL +4 -0
  27. 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}"