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,713 @@
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__(
112
+ self, operation: Operation, engine: str
113
+ ): # , schema_object: SchemaObject):
114
+ self.operation = operation
115
+ self.engine = engine
116
+ self.__select_list_columns = None
117
+
118
+ @property
119
+ def sql(self) -> str:
120
+ raise NotImplementedError("Subclasses must implement this method")
121
+
122
+ @property
123
+ def placeholders(self) -> Dict[str, SchemaObjectProperty]:
124
+ raise NotImplementedError("Subclasses must implement this method")
125
+
126
+ @property
127
+ def select_list_columns(self) -> List[str]:
128
+ if not self.__select_list_columns:
129
+ # Filter columns based on read permissions
130
+ self.__select_list_columns = list(self.selection_results.keys())
131
+ return self.__select_list_columns
132
+
133
+ def marshal_record(self, record: dict) -> dict:
134
+ """
135
+ Converts a database record to an API-compatible dictionary,
136
+ removing properties the user is not allowed to read.
137
+
138
+ Args:
139
+ record (dict): A dictionary representing a single database record.
140
+
141
+ Returns:
142
+ dict: A dictionary with only the properties the user is allowed to see.
143
+ """
144
+ result = {}
145
+ for name, value in record.items():
146
+ if name in self.selection_results: # Check if the property is allowed
147
+ property = self.selection_results[name]
148
+ result[property.api_name] = property.convert_to_api_value(value)
149
+ return result
150
+
151
+ def placeholder(self, property: SchemaObjectProperty, param: str = "") -> str:
152
+ if len(param) == 0:
153
+ param = property.api_name if property.api_name is not None else ""
154
+
155
+ if self.engine == "oracle":
156
+ if property.column_type == "date":
157
+ return f"TO_DATE(:{param}, 'YYYY-MM-DD')"
158
+ elif property.column_type == "datetime":
159
+ return f"TO_TIMESTAMP(:{param}, 'YYYY-MM-DD\"T\"HH24:MI:SS.FF')"
160
+ elif property.column_type == "time":
161
+ return f"TO_TIME(:{param}, 'HH24:MI:SS.FF')"
162
+ return f":{param}"
163
+ return f"%({param})s"
164
+
165
+ def check_permissions(
166
+ self,
167
+ permission_type: str,
168
+ permissions: Optional[dict],
169
+ properties: Dict[str, SchemaObjectProperty],
170
+ ) -> Dict[str, SchemaObjectProperty]:
171
+ """
172
+ Checks the user's permissions for the specified permission type.
173
+
174
+ Args:
175
+ permission_type (str): The type of permission to check ("read" or "write").
176
+ properties (List[str], optional): Specific properties to check. If None,
177
+ all schema properties are checked.
178
+
179
+ Returns:
180
+ List[str]: A list of properties the user is permitted to access.
181
+ """
182
+ # if permissions are not defined then no restrictions are applied
183
+ log.info(
184
+ f"checking permissions permission_type: {permission_type}, permissions: {permissions}"
185
+ )
186
+ if not permissions:
187
+ return properties
188
+
189
+ allowed_properties = {}
190
+ log.info(
191
+ "Input properties keys: %s",
192
+ list(properties.keys()) if properties else "None",
193
+ )
194
+ log.info("Operation roles: %s", self.operation.roles)
195
+
196
+ # Normalize permissions to provider-first format if needed
197
+ normalized_permissions = self._normalize_permissions(permissions)
198
+
199
+ # Permissions structure: {provider: {action: {role: rule}}}
200
+ # Use 'default' as the standard provider
201
+ provider_permissions = normalized_permissions.get("default", {})
202
+ action_permissions = provider_permissions.get(permission_type, {})
203
+
204
+ log.info(
205
+ "Extracted action permissions for %s: %s",
206
+ permission_type,
207
+ action_permissions,
208
+ )
209
+
210
+ for role in self.operation.roles:
211
+ role_permissions = action_permissions.get(role, {})
212
+ log.info("role: %s, role_permissions: %s", role, role_permissions)
213
+
214
+ # If no permissions found for specific role, check wildcard "*"
215
+ if not role_permissions:
216
+ role_permissions = action_permissions.get("*", {})
217
+ log.info("Fallback to wildcard role '*': %s", role_permissions)
218
+ if not role_permissions:
219
+ continue
220
+
221
+ # Extract permission pattern from the rule
222
+ regex_pattern = self._extract_permission_pattern(role_permissions)
223
+
224
+ if regex_pattern:
225
+ # Use efficient regex dictionary filtering
226
+ role_allowed = self._filter_properties_by_regex(
227
+ properties, regex_pattern
228
+ )
229
+ log.info(
230
+ "role: %s, pattern: %s, matched: %s",
231
+ role,
232
+ regex_pattern,
233
+ list(role_allowed.keys()),
234
+ )
235
+
236
+ # Merge with existing allowed properties
237
+ allowed_properties.update(role_allowed)
238
+
239
+ log.info("allowed_properties: %s", allowed_properties)
240
+ return allowed_properties
241
+
242
+ def _normalize_permissions(self, permissions: dict) -> dict:
243
+ """
244
+ Normalize permissions from legacy role-first format to provider-first format.
245
+
246
+ Legacy format: {role: {action: rule}}
247
+ New format: {provider: {action: {role: rule}}}
248
+
249
+ Args:
250
+ permissions: Raw permissions dictionary
251
+
252
+ Returns:
253
+ Normalized permissions in provider-first format
254
+ """
255
+ if not permissions or not isinstance(permissions, dict):
256
+ return {}
257
+
258
+ # Check if already in new format (has 'default' or other provider keys)
259
+ if "default" in permissions:
260
+ return permissions
261
+
262
+ # Check if this is legacy format: role -> action -> rule
263
+ is_legacy = False
264
+ for key, value in permissions.items():
265
+ if isinstance(value, dict):
266
+ # Check if value contains action keys
267
+ for action_key in value.keys():
268
+ if action_key in {"read", "write", "delete", "create", "update"}:
269
+ is_legacy = True
270
+ break
271
+ if is_legacy:
272
+ break
273
+
274
+ if not is_legacy:
275
+ # Assume it's already in provider format
276
+ return permissions
277
+
278
+ # Convert legacy format to new format
279
+ normalized = {"default": {"read": {}, "write": {}, "delete": {}}}
280
+
281
+ for role, actions in permissions.items():
282
+ if not isinstance(actions, dict):
283
+ continue
284
+
285
+ for action, rule in actions.items():
286
+ # Normalize action names: create/update -> write
287
+ normalized_action = (
288
+ "write" if action in ("create", "update") else action
289
+ )
290
+
291
+ if normalized_action in normalized["default"]:
292
+ normalized["default"][normalized_action][role] = rule
293
+
294
+ log.info("Normalized legacy permissions: %s -> %s", permissions, normalized)
295
+ return normalized
296
+
297
+ def _filter_properties_by_regex(
298
+ self, properties: Dict[str, SchemaObjectProperty], regex_pattern: str
299
+ ) -> Dict[str, SchemaObjectProperty]:
300
+ """Filter dictionary properties using a regex pattern.
301
+
302
+ Args:
303
+ properties: Dictionary of property name -> SchemaObjectProperty
304
+ regex_pattern: Regex pattern to match property names
305
+
306
+ Returns:
307
+ Dict[str, SchemaObjectProperty]: Filtered properties dict
308
+ """
309
+ if not regex_pattern:
310
+ return {}
311
+
312
+ try:
313
+ compiled_regex = re.compile(regex_pattern)
314
+ return {k: v for k, v in properties.items() if compiled_regex.match(k)}
315
+ except re.error as e:
316
+ log.warning("Invalid regex pattern '%s': %s", regex_pattern, e)
317
+ return {}
318
+
319
+ def _extract_permission_pattern(self, permission_rule) -> str:
320
+ """Extract the regex pattern from a permission rule.
321
+
322
+ Args:
323
+ permission_rule: Can be a string (regex) or dict with
324
+ 'properties'/'fields' key
325
+
326
+ Returns:
327
+ str: The regex pattern to match property names
328
+ """
329
+ if isinstance(permission_rule, str):
330
+ return permission_rule
331
+ elif isinstance(permission_rule, dict):
332
+ # Support both 'properties' (preferred) and 'fields' (legacy)
333
+ return permission_rule.get("properties") or permission_rule.get(
334
+ "fields", ""
335
+ )
336
+ else:
337
+ return ""
338
+
339
+ @property
340
+ def selection_results(self) -> Dict:
341
+ raise NotImplementedError()
342
+
343
+ def generate_sql_condition(
344
+ self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
345
+ ) -> str:
346
+ operand = "="
347
+ if isinstance(value, str):
348
+ parts = value.split("::", 1)
349
+ operand = RELATIONAL_TYPES.get(parts[0], "=") if len(parts) > 1 else "="
350
+ value_str = parts[-1]
351
+ elif isinstance(value, (datetime, date)):
352
+ value_str = value.isoformat()
353
+ else:
354
+ value_str = str(value)
355
+
356
+ column = f"{prefix}.{property.column_name}" if prefix else property.column_name
357
+ placeholder_name = (
358
+ f"{prefix}_{property.api_name}" if prefix else property.api_name
359
+ )
360
+
361
+ if operand in ["between", "not-between"]:
362
+ value_set = value_str.split(",")
363
+ 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
364
+ elif operand in ["in", "not-in"]:
365
+ value_set = value_str.split(",")
366
+ assignments = [
367
+ self.placeholder(property, f"{placeholder_name}_{index}")
368
+ for index, _ in enumerate(value_set)
369
+ ]
370
+ sql = f"{column} {'NOT ' if operand == 'not-in' else ''}IN ({', '.join(assignments)})" # noqa E501
371
+ else:
372
+ sql = f"{column} {operand} {self.placeholder(property, str(placeholder_name))}"
373
+ return sql
374
+
375
+ def generate_placeholders(
376
+ self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
377
+ ) -> dict:
378
+ operand = "="
379
+
380
+ if isinstance(value, str):
381
+ parts = value.split("::", 1)
382
+ operand = RELATIONAL_TYPES.get(parts[0], "=") if len(parts) > 1 else "="
383
+ value_str = parts[-1]
384
+ elif isinstance(value, (datetime, date)):
385
+ value_str = value.isoformat()
386
+ else:
387
+ value_str = str(value)
388
+
389
+ placeholder_name = (
390
+ f"{prefix}_{property.api_name}" if prefix else property.api_name
391
+ )
392
+ placeholders = {}
393
+
394
+ if operand in ["between", "not-between"]:
395
+ value_set = value_str.split(",")
396
+ placeholders = {
397
+ f"{placeholder_name}_1": property.convert_to_db_value(value_set[0]),
398
+ f"{placeholder_name}_2": property.convert_to_db_value(value_set[1]),
399
+ }
400
+ elif operand in ["in", "not-in"]:
401
+ value_set = value_str.split(",")
402
+ for index, item in enumerate(value_set):
403
+ item_name = f"{placeholder_name}_{index}"
404
+ placeholders[item_name] = property.convert_to_db_value(item)
405
+ else:
406
+ placeholders = {placeholder_name: property.convert_to_db_value(value_str)}
407
+
408
+ return placeholders
409
+
410
+ def search_value_assignment(
411
+ self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
412
+ ) -> tuple[str, dict]:
413
+ sql_condition = self.generate_sql_condition(property, value, prefix)
414
+ placeholders = self.generate_placeholders(property, value, prefix)
415
+ return sql_condition, placeholders
416
+
417
+ def extract_injected_value(self, inject_value_source: str):
418
+ """
419
+ Extract value from various sources for property injection.
420
+
421
+ Args:
422
+ inject_value_source: Source specification (e.g., "claim:sub",
423
+ "timestamp", "uuid", "env:VAR_NAME")
424
+
425
+ Returns:
426
+ The extracted value, or None if source not found
427
+
428
+ Raises:
429
+ ApplicationException: If source format is invalid
430
+ """
431
+ import os
432
+ import uuid
433
+
434
+ if inject_value_source.startswith("claim:"):
435
+ claim_key = inject_value_source[6:]
436
+ return self.operation.claims.get(claim_key)
437
+ elif inject_value_source == "timestamp":
438
+ return datetime.utcnow().isoformat()
439
+ elif inject_value_source == "date":
440
+ return date.today().isoformat()
441
+ elif inject_value_source == "uuid":
442
+ return str(uuid.uuid4())
443
+ elif inject_value_source.startswith("env:"):
444
+ env_key = inject_value_source[4:]
445
+ return os.environ.get(env_key)
446
+ else:
447
+ raise ApplicationException(
448
+ 400, f"Unknown inject value source: {inject_value_source}"
449
+ )
450
+
451
+
452
+ class SQLSchemaQueryHandler(SQLQueryHandler):
453
+ schema_object: SchemaObject
454
+
455
+ def __init__(
456
+ self, operation: Operation, schema_object: SchemaObject, engine: str
457
+ ) -> None:
458
+ super().__init__(operation, engine)
459
+ self.schema_object = schema_object
460
+ self.single_table = self.__single_table()
461
+ self.__select_list = None
462
+ self.__selection_result_map = None
463
+ self.search_placeholders = dict()
464
+ self.store_placeholders = dict()
465
+ self.active_prefixes = set()
466
+
467
+ @property
468
+ def sql(self) -> str:
469
+ raise NotImplementedError("Subclasses should implement this method")
470
+
471
+ @property
472
+ def placeholders(self) -> dict:
473
+ return {**self.search_placeholders, **self.store_placeholders}
474
+
475
+ @property
476
+ def prefix_map(self) -> Dict[str, str]:
477
+ if not hasattr(self, "_prefix_map"):
478
+ self._prefix_map = {}
479
+ for entity in [
480
+ self.schema_object.api_name,
481
+ *self.schema_object.relations.keys(),
482
+ ]:
483
+ entity_lower = entity.lower()
484
+ for i in range(1, len(entity_lower) + 1):
485
+ substring = entity_lower[:i]
486
+ if (
487
+ substring not in self._prefix_map.values()
488
+ and substring not in SQL_RESERVED_WORDS
489
+ ):
490
+ self._prefix_map[entity] = substring
491
+ break
492
+ return self._prefix_map
493
+
494
+ def __single_table(self) -> bool:
495
+ if len(self.prefix_map) == 1 or self.operation.action == "create":
496
+ return True
497
+ if ":" in self.operation.metadata_params.get("properties", ""):
498
+ return False
499
+ for param in self.operation.query_params.keys():
500
+ if "." in param:
501
+ return False
502
+ return True
503
+
504
+ @property
505
+ def select_list(self) -> str:
506
+ if not self.__select_list:
507
+ self.__select_list = ", ".join(self.select_list_columns)
508
+ return self.__select_list
509
+
510
+ @property
511
+ def table_expression(self) -> str:
512
+ return self.schema_object.qualified_name or ""
513
+
514
+ @property
515
+ def selection_results(self) -> Dict:
516
+ """
517
+ Filters the schema properties to include only those the user is allowed to read.
518
+
519
+ Returns:
520
+ dict: A dictionary of allowed schema properties.
521
+ """
522
+ log.info("selection_result")
523
+ if not hasattr(self, "__selection_results"):
524
+ log.info("prefix_map: %s", self.prefix_map)
525
+ filters = self.operation.metadata_params.get("_properties", ".*").split()
526
+ allowed_properties = self.check_permissions(
527
+ "read", self.schema_object.permissions, self.schema_object.properties
528
+ )
529
+ self.__selection_results = self.filter_and_prefix_keys(
530
+ filters, allowed_properties
531
+ )
532
+ return self.__selection_results
533
+
534
+ def _has_soft_delete_conflicts(self) -> Dict[str, bool]:
535
+ """
536
+ Detect conflicts between query parameters and soft delete exclusions.
537
+
538
+ Returns dict mapping property names to whether they have conflicts.
539
+ A conflict occurs when a query explicitly requests values that would
540
+ be filtered out by soft delete rules.
541
+ """
542
+ conflicts = {}
543
+ soft_delete_props = self.schema_object.get_soft_delete_properties()
544
+
545
+ for prop_name, prop in soft_delete_props.items():
546
+ strategy = prop.get_soft_delete_strategy()
547
+ config = prop.get_soft_delete_config()
548
+
549
+ # Check if this property is queried
550
+ query_value = self.operation.query_params.get(prop_name)
551
+ if not query_value:
552
+ conflicts[prop_name] = False
553
+ continue
554
+
555
+ has_conflict = False
556
+
557
+ if strategy == "exclude_values":
558
+ excluded_values = config.get("values", [])
559
+ # Check if query value matches any excluded value
560
+ if isinstance(query_value, list):
561
+ # Handle IN queries: ?status=archived,deleted
562
+ has_conflict = any(val in excluded_values for val in query_value)
563
+ else:
564
+ # Handle single value: ?status=archived
565
+ has_conflict = query_value in excluded_values
566
+
567
+ elif strategy == "boolean_flag":
568
+ active_value = config.get("active_value", True)
569
+ # Conflict if explicitly querying for inactive value
570
+ if isinstance(query_value, str):
571
+ # Convert string to boolean for comparison
572
+ query_bool = query_value.lower() in ("true", "1", "yes")
573
+ has_conflict = query_bool != active_value
574
+ elif isinstance(query_value, bool):
575
+ has_conflict = query_value != active_value
576
+
577
+ elif strategy == "null_check":
578
+ # Conflict if explicitly querying for null/None values
579
+ # This might be "null", "none", empty string, etc.
580
+ if isinstance(query_value, str):
581
+ has_conflict = query_value.lower() in ("null", "none", "")
582
+ else:
583
+ has_conflict = query_value is None
584
+
585
+ conflicts[prop_name] = has_conflict
586
+
587
+ return conflicts
588
+
589
+ def _soft_delete_where_clause(self) -> str:
590
+ """
591
+ Generate WHERE clause to filter out soft-deleted records.
592
+
593
+ Uses smart conflict detection - if query explicitly requests
594
+ soft-deleted values, those filters are skipped to allow access.
595
+ """
596
+ conditions = []
597
+
598
+ soft_delete_props = self.schema_object.get_soft_delete_properties()
599
+ conflicts = self._has_soft_delete_conflicts()
600
+
601
+ for prop_name, prop in soft_delete_props.items():
602
+ # Skip filtering if user explicitly queries for soft-deleted values
603
+ if conflicts.get(prop_name, False):
604
+ continue
605
+
606
+ strategy = prop.get_soft_delete_strategy()
607
+ config = prop.get_soft_delete_config()
608
+ column_name = prop.column_name
609
+
610
+ if strategy == "null_check":
611
+ conditions.append(f"{column_name} IS NULL")
612
+ elif strategy == "boolean_flag":
613
+ active_value = config.get("active_value", True)
614
+ conditions.append(f"{column_name} = {active_value}")
615
+ elif strategy == "exclude_values":
616
+ excluded_values = config.get("values", [])
617
+ if excluded_values:
618
+ # Format values for SQL IN clause
619
+ formatted_values = ", ".join(
620
+ f"'{val}'" if isinstance(val, str) else str(val)
621
+ for val in excluded_values
622
+ )
623
+ conditions.append(f"{column_name} NOT IN ({formatted_values})")
624
+
625
+ return " AND ".join(conditions) if conditions else ""
626
+
627
+ @property
628
+ def search_condition(self) -> str:
629
+ self.search_placeholders = {}
630
+ conditions = []
631
+
632
+ # Add soft delete filtering first
633
+ soft_delete_filter = self._soft_delete_where_clause()
634
+ if soft_delete_filter:
635
+ conditions.append(soft_delete_filter)
636
+
637
+ for name, value in self.operation.query_params.items():
638
+ if "." in name:
639
+ raise ApplicationException(
640
+ 400, "Selection on relations is not supported"
641
+ )
642
+ property = self.schema_object.properties.get(name)
643
+ if not property:
644
+ raise ApplicationException(
645
+ 500, f"Search condition column not found {name}"
646
+ )
647
+ if (
648
+ self.operation.action != "read"
649
+ and isinstance(value, str)
650
+ and re.match(
651
+ r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value
652
+ )
653
+ and self.schema_object.concurrency_property
654
+ ):
655
+ raise ApplicationException(
656
+ 400,
657
+ "Concurrency settings prohibit multi-record updates "
658
+ + str(self.schema_object.api_name)
659
+ + ", property: "
660
+ + str(property.api_name),
661
+ )
662
+
663
+ assignment, holders = self.search_value_assignment(property, value)
664
+ conditions.append(assignment)
665
+ self.search_placeholders.update(holders)
666
+ return f" WHERE {' AND '.join(conditions)}" if conditions else ""
667
+
668
+ def filter_and_prefix_keys(
669
+ self, regex_list: List[str], properties: dict, prefix: Optional[str] = None
670
+ ) -> dict:
671
+ """
672
+ Accepts a prefix string, list of regular expressions, and a dictionary.
673
+ Returns a new dictionary containing items whose keys match any of the
674
+ regular expressions, with the prefix string prepended to the key
675
+ values of the dictionary.
676
+
677
+ Parameters:
678
+ - prefix (str): The prefix string to prepend to the key values.
679
+ - regex_list (list of str): The list of regular expression patterns
680
+ to match keys.
681
+ - properties (dict): The input properties.
682
+
683
+ Returns:
684
+ - dict: A new dictionary containing filtered items with modified key values.
685
+ """
686
+ filtered_dict = {}
687
+ compiled_regexes = [re.compile(regex) for regex in regex_list]
688
+ for key, value in properties.items():
689
+ for pattern in compiled_regexes:
690
+ if pattern.match(key):
691
+ filtered_dict[f"{prefix}.{key}" if prefix else key] = value
692
+ self.active_prefixes.add(prefix)
693
+ break
694
+ return filtered_dict
695
+
696
+ def concurrency_generator(self, property: SchemaObjectProperty) -> str:
697
+ if property.api_type == "date-time":
698
+ return "CURRENT_TIMESTAMP"
699
+ elif property.api_type == "integer":
700
+ return f"{property.column_name} + 1"
701
+ elif property.api_type in ["string", "uuid"]:
702
+ if self.engine == "oracle":
703
+ return "SYS_GUID()"
704
+ if self.engine == "mysql":
705
+ return "UUID()"
706
+ return "gen_random_uuid()"
707
+ raise ApplicationException(
708
+ 500,
709
+ (
710
+ "Concurrency control property is unrecognized type"
711
+ + f"name: {property.api_name}, type: {property.api_type}"
712
+ ),
713
+ )