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.
- api_foundry_query_engine/.pre-commit-config.yaml +22 -0
- api_foundry_query_engine/__init__.py +1 -0
- api_foundry_query_engine/adapters/adapter.py +73 -0
- api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
- api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
- api_foundry_query_engine/adapters/security_adapter.py +106 -0
- api_foundry_query_engine/connectors/connection.py +32 -0
- api_foundry_query_engine/connectors/connection_factory.py +115 -0
- api_foundry_query_engine/connectors/oracle_connector.py +29 -0
- api_foundry_query_engine/connectors/postgres_connection.py +142 -0
- api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
- api_foundry_query_engine/dao/dao.py +23 -0
- api_foundry_query_engine/dao/operation_dao.py +186 -0
- api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
- api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
- api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
- api_foundry_query_engine/dao/sql_query_handler.py +713 -0
- api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
- api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
- api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
- api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
- api_foundry_query_engine/lambda_handler.py +63 -0
- api_foundry_query_engine/operation.py +104 -0
- api_foundry_query_engine/services/service.py +75 -0
- api_foundry_query_engine/services/transactional_service.py +52 -0
- api_foundry_query_engine/utils/api_model.py +380 -0
- api_foundry_query_engine/utils/app_exception.py +22 -0
- api_foundry_query_engine/utils/claims_check.py +471 -0
- api_foundry_query_engine/utils/dependency_resolver.py +157 -0
- api_foundry_query_engine/utils/gateway_operation.py +279 -0
- api_foundry_query_engine/utils/logger.py +60 -0
- api_foundry_query_engine/utils/reference_resolver.py +222 -0
- api_foundry_query_engine/utils/token_decoder.py +624 -0
- api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
- api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
- api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
- 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
|