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,142 @@
1
+ from api_foundry_query_engine.connectors.connection import Connection, Cursor
2
+ from api_foundry_query_engine.utils.logger import logger
3
+
4
+ # Initialize the logger
5
+ log = logger(__name__)
6
+
7
+
8
+ class PostgresCursor(Cursor):
9
+ def __init__(self, cursor):
10
+ self.__cursor = cursor
11
+
12
+ def execute(self, sql: str, params: dict, selection_results: dict) -> list[dict]:
13
+ """
14
+ Execute SQL statements on the PostgreSQL database.
15
+
16
+ Parameters:
17
+ - cursor: The database cursor.
18
+ - sql (str): The SQL statement to execute.
19
+ - params (dict): Parameters to be used in the SQL statement.
20
+ - selection_results (dict): Mapping of result columns.
21
+
22
+ Returns:
23
+ - list[dict]: List of result records as dictionaries.
24
+
25
+ Raises:
26
+ - AppException: Custom exception for handling database-related errors.
27
+ """
28
+ from psycopg2 import Error, IntegrityError, ProgrammingError
29
+
30
+ log.info("sql: %s", sql)
31
+
32
+ try:
33
+ # Execute the SQL statement with parameters
34
+ self.__cursor.execute(sql, params)
35
+ result = []
36
+ for record in self.__cursor:
37
+ # Convert record tuple to dictionary using selection_results
38
+ result.append(
39
+ {col: value for col, value in zip(selection_results, record)}
40
+ )
41
+
42
+ return result
43
+ except IntegrityError as err:
44
+ # Handle integrity constraint violation (e.g., duplicate key)
45
+ from api_foundry_query_engine.utils.app_exception import (
46
+ ApplicationException,
47
+ )
48
+
49
+ raise ApplicationException(409, err.pgerror)
50
+ except ProgrammingError as err:
51
+ # Handle programming errors (e.g., syntax error in SQL)
52
+ from api_foundry_query_engine.utils.app_exception import (
53
+ ApplicationException,
54
+ )
55
+
56
+ raise ApplicationException(400, err.pgerror)
57
+ except Error as err:
58
+ # Handle other database errors
59
+ from api_foundry_query_engine.utils.app_exception import (
60
+ ApplicationException,
61
+ )
62
+
63
+ raise ApplicationException(500, err.pgerror)
64
+
65
+ def close(self):
66
+ self.__cursor.close()
67
+
68
+
69
+ class PostgresConnection(Connection):
70
+ """
71
+ PostgreSQL database connection wrapper.
72
+
73
+ Supports two configuration formats:
74
+ 1. DSN-based (preferred for testing with fixture_foundry):
75
+ {"dsn": "postgresql://user:pass@host:port/dbname"}
76
+
77
+ 2. Individual parameters (for production AWS Secrets Manager):
78
+ {"host": "...", "port": 5432, "database": "...", "username": "...", "password": "..."}
79
+
80
+ The get_connection() method prioritizes DSN if present, otherwise builds connection
81
+ from individual parameters.
82
+ """
83
+
84
+ def __init__(self, db_config: dict) -> None:
85
+ super().__init__(db_config)
86
+ self.__connection = self.get_connection()
87
+
88
+ def cursor(self) -> Cursor:
89
+ return PostgresCursor(self.__connection.cursor())
90
+
91
+ def close(self):
92
+ self.__connection.close()
93
+
94
+ def commit(self):
95
+ self.__connection.commit()
96
+
97
+ def rollback(self):
98
+ self.__connection.rollback()
99
+
100
+ def get_connection(self):
101
+ """
102
+ Get a connection to the PostgreSQL database.
103
+
104
+ Parameters:
105
+ - schema (str, optional): The database schema to set for the
106
+ connection.
107
+
108
+ Returns:
109
+ - connection: A connection to the PostgreSQL database.
110
+ """
111
+ from psycopg2 import connect
112
+
113
+ # If DSN is provided, use it directly (simplifies fixture_foundry integration)
114
+ if "dsn" in self.db_config:
115
+ log.info("Connecting using DSN: %s", self.db_config["dsn"])
116
+ return connect(self.db_config["dsn"])
117
+
118
+ # Otherwise, build connection from individual parameters
119
+ dbname = self.db_config["database"]
120
+ user = self.db_config["username"]
121
+ password = self.db_config["password"]
122
+ host = self.db_config.get("host", "localhost")
123
+ port = self.db_config.get("port", 5432)
124
+ additional_config = self.db_config.get("configuration", {})
125
+
126
+ # Merge additional configuration parameters with the main connection parameters
127
+ connection_params = {
128
+ "dbname": dbname,
129
+ "user": user,
130
+ "password": password,
131
+ "host": host,
132
+ "port": port,
133
+ }
134
+
135
+ connection_params.update(additional_config)
136
+
137
+ log.info(
138
+ f"connection_params: dbname: {dbname}, user: {user}, host: {host}, port: {port}"
139
+ )
140
+
141
+ # Create a connection to the PostgreSQL database
142
+ return connect(**connection_params)
@@ -0,0 +1,295 @@
1
+ """
2
+ Batch Operation Handler
3
+
4
+ Orchestrates execution of multiple operations with dependency resolution,
5
+ reference substitution, and transaction management.
6
+ """
7
+
8
+ from typing import Dict, Any, List, Optional
9
+ from api_foundry_query_engine.operation import Operation
10
+ from api_foundry_query_engine.dao.operation_dao import OperationDAO
11
+ from api_foundry_query_engine.utils.dependency_resolver import DependencyResolver
12
+ from api_foundry_query_engine.utils.reference_resolver import ReferenceResolver
13
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
14
+ from api_foundry_query_engine.utils.logger import logger
15
+
16
+ log = logger(__name__)
17
+
18
+
19
+ class BatchOperationHandler:
20
+ """Handles execution of batch operations with dependencies."""
21
+
22
+ def __init__(self, batch_request: Dict[str, Any], connection, engine: str):
23
+ """
24
+ Initialize the batch operation handler.
25
+
26
+ Args:
27
+ batch_request: Batch request with 'operations' and 'options'
28
+ connection: Database connection for executing operations
29
+ engine: Database engine type (postgres, mysql, etc.)
30
+ """
31
+ self.operations = batch_request.get("operations", [])
32
+ self.options = batch_request.get("options", {})
33
+ self.connection = connection
34
+ self.engine = engine
35
+ self.results: Dict[str, Dict[str, Any]] = {}
36
+ self.failed_operations: List[str] = []
37
+
38
+ # Validate batch request
39
+ self._validate_batch_request()
40
+
41
+ # Resolve execution order
42
+ self.resolver = DependencyResolver(self.operations)
43
+ self.execution_order = self.resolver.get_execution_order()
44
+
45
+ def _validate_batch_request(self):
46
+ """Validate the batch request structure."""
47
+ if not self.operations:
48
+ raise ApplicationException(
49
+ 400, "Batch request must contain at least one operation"
50
+ )
51
+
52
+ if len(self.operations) > 100:
53
+ raise ApplicationException(
54
+ 400,
55
+ f"Batch size exceeds maximum (100). "
56
+ f"Requested: {len(self.operations)}",
57
+ )
58
+
59
+ # Auto-generate IDs for operations that don't have them
60
+ seen_ids = set()
61
+ for i, op in enumerate(self.operations):
62
+ # Auto-generate ID if not provided
63
+ if "id" not in op or not op["id"]:
64
+ op["id"] = f"op_{i}"
65
+
66
+ # Check for duplicate IDs
67
+ if op["id"] in seen_ids:
68
+ raise ApplicationException(
69
+ 400,
70
+ f"Duplicate operation ID '{op['id']}' found",
71
+ )
72
+ seen_ids.add(op["id"])
73
+
74
+ # Validate required fields
75
+ if "entity" not in op:
76
+ raise ApplicationException(
77
+ 400,
78
+ f"Operation '{op['id']}' missing required field 'entity'",
79
+ )
80
+ if "action" not in op:
81
+ raise ApplicationException(
82
+ 400,
83
+ f"Operation '{op['id']}' missing required field 'action'",
84
+ )
85
+ if op["action"] not in ["create", "read", "update", "delete"]:
86
+ raise ApplicationException(
87
+ 400,
88
+ f"Operation '{op['id']}' has invalid action " f"'{op['action']}'",
89
+ )
90
+
91
+ def execute(self) -> Dict[str, Any]:
92
+ """
93
+ Execute all operations in dependency order.
94
+
95
+ Returns:
96
+ Dictionary with 'success', 'results', and optionally 'errors'
97
+
98
+ Raises:
99
+ ApplicationException: On validation or execution errors
100
+ """
101
+ atomic = self.options.get("atomic", True)
102
+ continue_on_error = self.options.get("continueOnError", False)
103
+
104
+ log.info(
105
+ "Executing batch with %d operations (atomic=%s, continue=%s)",
106
+ len(self.operations),
107
+ atomic,
108
+ continue_on_error,
109
+ )
110
+
111
+ try:
112
+ for op_id in self.execution_order:
113
+ # Check if we should skip this operation
114
+ if self._should_skip_operation(op_id):
115
+ self.results[op_id] = {
116
+ "status": "skipped",
117
+ "reason": "Dependency failed",
118
+ }
119
+ continue
120
+
121
+ # Execute the operation
122
+ try:
123
+ result = self._execute_operation(op_id)
124
+ # Unwrap single-item lists for easier reference access
125
+ # e.g., $ref:op_0.invoice_id instead of $ref:op_0.0.invoice_id
126
+ if isinstance(result, list) and len(result) == 1:
127
+ result = result[0]
128
+ self.results[op_id] = {"status": "completed", "data": result}
129
+ log.info("Operation '%s' completed successfully", op_id)
130
+
131
+ except ApplicationException as e:
132
+ log.error("Operation '%s' failed: %s", op_id, e.message)
133
+ self.results[op_id] = {
134
+ "status": "failed",
135
+ "error": e.message,
136
+ "statusCode": e.status_code,
137
+ }
138
+ self.failed_operations.append(op_id)
139
+
140
+ # Rollback the failed transaction
141
+ # In non-atomic mode, this allows next operation to proceed
142
+ if not atomic:
143
+ self.connection.rollback()
144
+ log.info("Failed operation rolled back (non-atomic mode)")
145
+
146
+ # Stop on error if not continuing
147
+ if not continue_on_error:
148
+ if atomic:
149
+ self.connection.rollback()
150
+ log.info("Transaction rolled back")
151
+ raise ApplicationException(
152
+ 400,
153
+ f"Batch failed at operation '{op_id}': {e.message}",
154
+ ) from e
155
+
156
+ # Commit each operation if not atomic and it succeeded
157
+ if not atomic and op_id not in self.failed_operations:
158
+ self.connection.commit()
159
+ log.info("Operation '%s' committed (non-atomic mode)", op_id)
160
+
161
+ # Commit transaction if atomic and all succeeded
162
+ if atomic and not self.failed_operations:
163
+ self.connection.commit()
164
+ log.info("Batch transaction committed successfully")
165
+ elif atomic and self.failed_operations and continue_on_error:
166
+ self.connection.rollback()
167
+ log.info("Batch had failures, transaction rolled back")
168
+
169
+ # Build response
170
+ success = len(self.failed_operations) == 0
171
+ response = {"success": success, "results": self.results}
172
+
173
+ if self.failed_operations:
174
+ response["failedOperations"] = self.failed_operations
175
+
176
+ return response
177
+
178
+ except Exception as e:
179
+ # Rollback on any unexpected error
180
+ if atomic:
181
+ self.connection.rollback()
182
+ log.error("Batch failed, transaction rolled back")
183
+ raise e
184
+
185
+ def _should_skip_operation(self, op_id: str) -> bool:
186
+ """
187
+ Determine if operation should be skipped due to failed dependencies.
188
+
189
+ Args:
190
+ op_id: Operation ID to check
191
+
192
+ Returns:
193
+ True if operation should be skipped
194
+ """
195
+ op = next(o for o in self.operations if o["id"] == op_id)
196
+ depends_on = op.get("depends_on", [])
197
+
198
+ for dep_id in depends_on:
199
+ if dep_id in self.failed_operations:
200
+ log.info(
201
+ "Skipping operation '%s' due to failed dependency '%s'",
202
+ op_id,
203
+ dep_id,
204
+ )
205
+ return True
206
+ if dep_id not in self.results:
207
+ log.warning(
208
+ "Operation '%s' depends on '%s' which hasn't executed yet",
209
+ op_id,
210
+ dep_id,
211
+ )
212
+ return True
213
+ if self.results[dep_id].get("status") != "completed":
214
+ log.info(
215
+ "Skipping operation '%s' due to incomplete dependency '%s'",
216
+ op_id,
217
+ dep_id,
218
+ )
219
+ return True
220
+
221
+ return False
222
+
223
+ def _execute_operation(self, op_id: str) -> Any:
224
+ """
225
+ Execute a single operation.
226
+
227
+ Args:
228
+ op_id: Operation ID to execute
229
+
230
+ Returns:
231
+ Operation result data
232
+
233
+ Raises:
234
+ ApplicationException: On execution error
235
+ """
236
+ # Get operation definition
237
+ op_def = next(o for o in self.operations if o["id"] == op_id)
238
+
239
+ # Resolve references in parameters
240
+ ref_resolver = ReferenceResolver(self.results)
241
+ query_params = ref_resolver.resolve_parameters(
242
+ op_def.get("query_params", {}), op_id
243
+ )
244
+ store_params = ref_resolver.resolve_parameters(
245
+ op_def.get("store_params", {}), op_id
246
+ )
247
+ metadata_params = ref_resolver.resolve_parameters(
248
+ op_def.get("metadata_params", {}), op_id
249
+ )
250
+
251
+ # Get claims from operation or use empty dict
252
+ claims = op_def.get("claims", {})
253
+
254
+ # Create Operation object
255
+ operation = Operation(
256
+ entity=op_def["entity"],
257
+ action=op_def["action"],
258
+ query_params=query_params,
259
+ store_params=store_params,
260
+ metadata_params=metadata_params,
261
+ claims=claims,
262
+ )
263
+
264
+ log.debug(
265
+ "Executing operation '%s': %s %s",
266
+ op_id,
267
+ operation.action,
268
+ operation.entity,
269
+ )
270
+
271
+ # Execute through OperationDAO
272
+ dao = OperationDAO(operation, self.engine)
273
+ result = dao.execute(self.connection)
274
+
275
+ return result
276
+
277
+ def get_operation_summary(self) -> Dict[str, int]:
278
+ """
279
+ Get summary statistics of batch execution.
280
+
281
+ Returns:
282
+ Dictionary with counts of completed, failed, skipped operations
283
+ """
284
+ summary = {"total": len(self.operations), "completed": 0, "failed": 0}
285
+
286
+ for op_result in self.results.values():
287
+ status = op_result.get("status")
288
+ if status == "completed":
289
+ summary["completed"] += 1
290
+ elif status == "failed":
291
+ summary["failed"] += 1
292
+ elif status == "skipped":
293
+ summary["skipped"] = summary.get("skipped", 0) + 1
294
+
295
+ return summary
@@ -0,0 +1,23 @@
1
+ import abc
2
+ from typing import Union
3
+
4
+ from api_foundry_query_engine.connectors.connection import Connection
5
+ from api_foundry_query_engine.operation import Operation
6
+
7
+
8
+ class DAO(metaclass=abc.ABCMeta):
9
+ @classmethod
10
+ def __subclasshook__(cls, __subclass: type) -> bool:
11
+ return hasattr(__subclass, "execute") and callable(__subclass.execute)
12
+
13
+ def execute(
14
+ self, connector: Connection, operation: Operation
15
+ ) -> Union[list[dict], dict]:
16
+ raise NotImplementedError
17
+
18
+
19
+ class DAOAdapter(DAO):
20
+ def execute(
21
+ self, connector: Connection, operation: Operation
22
+ ) -> Union[list[dict], dict]:
23
+ return super().execute(connector, operation)
@@ -0,0 +1,186 @@
1
+ from typing import Union
2
+
3
+ from api_foundry_query_engine.dao.sql_custom_query_handler import SQLCustomQueryHandler
4
+ from api_foundry_query_engine.dao.sql_delete_query_handler import (
5
+ SQLDeleteSchemaQueryHandler,
6
+ )
7
+ from api_foundry_query_engine.dao.sql_insert_query_handler import (
8
+ SQLInsertSchemaQueryHandler,
9
+ )
10
+ from api_foundry_query_engine.dao.sql_select_query_handler import (
11
+ SQLSelectSchemaQueryHandler,
12
+ )
13
+ from api_foundry_query_engine.dao.sql_subselect_query_handler import (
14
+ SQLSubselectSchemaQueryHandler,
15
+ )
16
+ from api_foundry_query_engine.dao.sql_update_query_handler import (
17
+ SQLUpdateSchemaQueryHandler,
18
+ )
19
+ from api_foundry_query_engine.dao.sql_restore_query_handler import (
20
+ SQLRestoreSchemaQueryHandler,
21
+ )
22
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
23
+ from api_foundry_query_engine.dao.dao import DAO
24
+ from api_foundry_query_engine.connectors.connection import Cursor
25
+ from api_foundry_query_engine.operation import Operation
26
+ from api_foundry_query_engine.utils.api_model import (
27
+ get_path_operation,
28
+ get_schema_object,
29
+ )
30
+ from api_foundry_query_engine.dao.sql_query_handler import SQLQueryHandler
31
+
32
+
33
+ class OperationDAO(DAO):
34
+ """
35
+ A class to handle database operations based on the provided
36
+ Operation object.
37
+
38
+ Attributes:
39
+ operation (Operation): The operation to perform.
40
+ """
41
+
42
+ def __init__(self, operation: Operation, engine: str) -> None:
43
+ """
44
+ Initialize the OperationDAO with the provided Operation object.
45
+
46
+ Args:
47
+ operation (Operation): The operation to perform.
48
+ """
49
+ super().__init__()
50
+ self.operation = operation
51
+ self.engine = engine
52
+
53
+ @property
54
+ def query_handler(self) -> SQLQueryHandler:
55
+ if not hasattr(self, "_query_handler"):
56
+ path_operation = get_path_operation(
57
+ self.operation.entity, self.operation.action
58
+ )
59
+ if path_operation:
60
+ self._query_handler = SQLCustomQueryHandler(
61
+ self.operation, path_operation, self.engine
62
+ )
63
+ return self._query_handler
64
+
65
+ schema_object = get_schema_object(self.operation.entity)
66
+ if not schema_object:
67
+ raise ApplicationException(
68
+ 500, f"Unknown operation: {self.operation.entity}"
69
+ )
70
+ if self.operation.action == "read":
71
+ self._query_handler = SQLSelectSchemaQueryHandler(
72
+ self.operation, schema_object, self.engine
73
+ )
74
+ elif self.operation.action == "create":
75
+ self._query_handler = SQLInsertSchemaQueryHandler(
76
+ self.operation, schema_object, self.engine
77
+ )
78
+ elif self.operation.action == "update":
79
+ self._query_handler = SQLUpdateSchemaQueryHandler(
80
+ self.operation, schema_object, self.engine
81
+ )
82
+ elif self.operation.action == "delete":
83
+ self._query_handler = SQLDeleteSchemaQueryHandler(
84
+ self.operation, schema_object, self.engine
85
+ )
86
+ elif self.operation.action == "restore":
87
+ self._query_handler = SQLRestoreSchemaQueryHandler(
88
+ self.operation, schema_object, self.engine
89
+ )
90
+ else:
91
+ raise ApplicationException(
92
+ 400, f"Invalid operation action: {self.operation.action}"
93
+ )
94
+ return self._query_handler
95
+
96
+ def execute(self, connector, operation=None) -> Union[list[dict], dict]:
97
+ """
98
+ Execute the database operation based on the provided connector.
99
+
100
+ Args:
101
+ connector (Connection): The database connection.
102
+ operation (Operation, optional): The operation to perform.
103
+
104
+ Returns:
105
+ list[dict]: A list of dictionaries containing the results
106
+ of the operation.
107
+ """
108
+
109
+ # Use self.operation if operation is not provided
110
+ op = operation if operation is not None else self.operation
111
+
112
+ # Check if this is a batch operation
113
+ if op.entity == "batch" and op.action == "create":
114
+ from api_foundry_query_engine.dao.batch_operation_handler import (
115
+ BatchOperationHandler,
116
+ )
117
+
118
+ # Extract batch request from store_params
119
+ batch_request = op.store_params
120
+
121
+ # Execute batch
122
+ handler = BatchOperationHandler(batch_request, connector, self.engine)
123
+ return handler.execute()
124
+
125
+ # Standard operation handling
126
+ # Assume connector has a 'cursor()' method to get a Cursor
127
+ cursor = connector.cursor()
128
+
129
+ result = self.__fetch_record_set(self.query_handler, cursor)
130
+
131
+ if op.action == "read":
132
+ if op.metadata_params.get("count", False):
133
+ return result[0]
134
+ self.__fetch_many(result, cursor)
135
+ elif op.action in ["update", "delete", "restore"] and len(result) == 0:
136
+ raise ApplicationException(400, "No records were modified")
137
+
138
+ return result
139
+
140
+ def __fetch_many(self, parent_set: list[dict], cursor: Cursor):
141
+ if "properties" not in self.operation.metadata_params:
142
+ return
143
+
144
+ schema_object = get_schema_object(self.operation.entity)
145
+ for name, relation in schema_object.relations.items():
146
+ if relation.type == "object":
147
+ continue
148
+
149
+ child_set = self.__fetch_record_set(
150
+ SQLSubselectSchemaQueryHandler(
151
+ self.operation, relation, self.query_handler # type: ignore
152
+ ),
153
+ cursor,
154
+ )
155
+ if len(child_set) == 0:
156
+ continue
157
+
158
+ for parent in parent_set:
159
+ parent[name] = []
160
+
161
+ parents = {}
162
+ for parent in parent_set:
163
+ parents[parent[relation.parent_property]] = parent
164
+
165
+ for child in child_set:
166
+ parent_id = child[relation.child_property]
167
+ parent = parents.get(parent_id)
168
+ if parent:
169
+ parent[name].append(child)
170
+
171
+ def __fetch_record_set(
172
+ self, query_handler: SQLQueryHandler, cursor: Cursor
173
+ ) -> list[dict]:
174
+ sql = query_handler.sql
175
+ if not sql:
176
+ return []
177
+
178
+ record_set = cursor.execute(
179
+ sql, query_handler.placeholders, query_handler.selection_results
180
+ )
181
+ result = []
182
+ for record in record_set:
183
+ object = query_handler.marshal_record(record)
184
+ result.append(object)
185
+
186
+ return result