jaaql-middleware-python 5.3.12__tar.gz → 5.3.14__tar.gz

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 (78) hide show
  1. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/PKG-INFO +1 -1
  2. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/constants.py +1 -1
  3. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/db/db_interface.py +5 -3
  4. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/db/db_pg_interface.py +8 -1
  5. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/interpreter/interpret_jaaql.py +175 -7
  6. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/model.py +30 -19
  7. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql_middleware_python.egg-info/PKG-INFO +1 -1
  8. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/LICENSE.txt +0 -0
  9. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/README.md +0 -0
  10. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/__init__.py +0 -0
  11. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/config/__init__.py +0 -0
  12. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/config/config-docker.ini +0 -0
  13. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/config/config-test.ini +0 -0
  14. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/config/config.ini +0 -0
  15. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/config_constants.py +0 -0
  16. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/db/__init__.py +0 -0
  17. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/db/db_utils.py +0 -0
  18. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/db/db_utils_no_circ.py +0 -0
  19. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/documentation/__init__.py +0 -0
  20. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/documentation/documentation_internal.py +0 -0
  21. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/documentation/documentation_public.py +0 -0
  22. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/documentation/documentation_shared.py +0 -0
  23. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/email/__init__.py +0 -0
  24. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/email/email_manager.py +0 -0
  25. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/email/email_manager_service.py +0 -0
  26. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/email/patch_ems.py +0 -0
  27. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/exceptions/__init__.py +0 -0
  28. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/exceptions/custom_http_status.py +0 -0
  29. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/exceptions/http_status_exception.py +0 -0
  30. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/exceptions/jaaql_interpretable_handled_errors.py +0 -0
  31. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/exceptions/not_yet_implement_exception.py +0 -0
  32. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/generated_constants.py +0 -0
  33. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/interpreter/__init__.py +0 -0
  34. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/jaaql.py +0 -0
  35. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/migrations/__init__.py +0 -0
  36. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/migrations/migrations.py +0 -0
  37. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/__init__.py +0 -0
  38. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/base_controller.py +0 -0
  39. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/base_model.py +0 -0
  40. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/controller.py +0 -0
  41. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/controller_interface.py +0 -0
  42. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/exception_queries.py +0 -0
  43. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/generated_queries.py +0 -0
  44. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/handmade_queries.py +0 -0
  45. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/model_interface.py +0 -0
  46. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/mvc/response.py +0 -0
  47. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/openapi/__init__.py +0 -0
  48. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/openapi/swagger_documentation.py +0 -0
  49. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/patch.py +0 -0
  50. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/01.install_domains.generated.sql +0 -0
  51. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/02.install_super_user.exceptions.sql +0 -0
  52. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/03.install_super_user.handwritten.sql +0 -0
  53. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/04.install_jaaql_data_structures.generated.sql +0 -0
  54. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/05.install_static_data.generated.sql +0 -0
  55. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/06.install_jaaql.exceptions.sql +0 -0
  56. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/ZZZZ.generated_functions_views_and_permissions.sql +0 -0
  57. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/ZZZZ.reset_references.sql +0 -0
  58. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/scripts/swagger_template.html +0 -0
  59. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/services/__init__.py +0 -0
  60. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/services/cached_canned_query_service.py +0 -0
  61. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/services/migrations_manager_service.py +0 -0
  62. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/services/patch_mms.py +0 -0
  63. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/services/patch_shared_var_service.py +0 -0
  64. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/services/shared_var_service.py +0 -0
  65. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/__init__.py +0 -0
  66. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/bootstrap_secrets.py +0 -0
  67. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/cron.py +0 -0
  68. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/crypt_utils.py +0 -0
  69. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/options.py +0 -0
  70. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/utils.py +0 -0
  71. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/utils_no_project_imports.py +0 -0
  72. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql/utilities/vault.py +0 -0
  73. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql_middleware_python.egg-info/SOURCES.txt +0 -0
  74. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql_middleware_python.egg-info/dependency_links.txt +0 -0
  75. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql_middleware_python.egg-info/requires.txt +0 -0
  76. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/jaaql_middleware_python.egg-info/top_level.txt +0 -0
  77. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/setup.cfg +0 -0
  78. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.14}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaaql-middleware-python
3
- Version: 5.3.12
3
+ Version: 5.3.14
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -218,5 +218,5 @@ ROLE__dba = "dba"
218
218
 
219
219
  PROTOCOL__postgres = "postgresql://"
220
220
 
221
- VERSION = "5.3.12"
221
+ VERSION = "5.3.14"
222
222
 
@@ -153,7 +153,7 @@ class DBInterface(ABC):
153
153
  pass
154
154
 
155
155
  def execute_query_fetching_results(self, conn, query, parameters=None, echo=ECHO__none, as_objects=False, wait_hook: queue.Queue = None,
156
- requires_dba_check: bool = False, prepare: bool = False):
156
+ requires_dba_check: bool = False, prepare: bool = False, capture_provenance: list = None):
157
157
  if echo not in ECHO__allowed:
158
158
  allowed_echoes = ", ".join([str(allowed_echo) for allowed_echo in ECHO__allowed])
159
159
  raise HttpStatusException(ERR__unknown_echo % (str(echo), allowed_echoes), HTTPStatus.BAD_REQUEST)
@@ -177,7 +177,8 @@ class DBInterface(ABC):
177
177
  if wait_hook:
178
178
  wait_hook = None
179
179
 
180
- columns, type_codes, rows = self.execute_query(conn, query, new_parameters, wait_hook, prepare=prepare)
180
+ columns, type_codes, rows = self.execute_query(conn, query, new_parameters, wait_hook, prepare=prepare,
181
+ capture_provenance=capture_provenance)
181
182
 
182
183
  ret = {
183
184
  RET__columns: columns,
@@ -253,7 +254,8 @@ class DBInterface(ABC):
253
254
  pass
254
255
 
255
256
  @abstractmethod
256
- def execute_query(self, conn, query, parameters: Optional[dict] = None, wait_hook: queue.Queue = None, prepare: bool = False):
257
+ def execute_query(self, conn, query, parameters: Optional[dict] = None, wait_hook: queue.Queue = None, prepare: bool = False,
258
+ capture_provenance: list = None):
257
259
  pass
258
260
 
259
261
  @abstractmethod
@@ -327,7 +327,8 @@ class DBPGInterface(DBInterface):
327
327
  return HttpStatusException(ERR__invalid_token, HTTPStatus.UNAUTHORIZED)
328
328
  return None
329
329
 
330
- def execute_query(self, conn, query, parameters=None, wait_hook: queue.Queue = None, prepare: bool = False):
330
+ def execute_query(self, conn, query, parameters=None, wait_hook: queue.Queue = None, prepare: bool = False,
331
+ capture_provenance: list = None):
331
332
  x = 0
332
333
  err = None
333
334
  while x < PGCONN__max_conns:
@@ -402,6 +403,12 @@ class DBPGInterface(DBInterface):
402
403
  if cursor.description is None:
403
404
  return [], [], []
404
405
  else:
406
+ if capture_provenance is not None:
407
+ capture_provenance.clear()
408
+ pgresult = cursor.pgresult
409
+ capture_provenance.extend(
410
+ (pgresult.ftype(idx), pgresult.ftable(idx), pgresult.ftablecol(idx))
411
+ for idx in range(pgresult.nfields))
405
412
  return [desc[0] for desc in cursor.description], [desc.type_code for desc in cursor.description], cursor.fetchall()
406
413
  except OperationalError as ex:
407
414
  if ex.sqlstate is None or ex.sqlstate.startswith("08") or ex.sqlstate.startswith("57"):
@@ -375,7 +375,32 @@ class InterpretJAAQL:
375
375
  last_query = self.encrypt_literals(last_query, encryption_key)
376
376
  found_params = {**found_parameter_dictionary, **enc_parameter_dictionary}
377
377
 
378
+ temp_view_name = None
379
+ provenance_res = None
380
+
378
381
  if attempt_fetch_domain_types:
382
+ described, provenance = self.describe_query_provenance(
383
+ conn, last_query, found_params, wait_hook=wait_hook,
384
+ requires_dba_check=check_required and canned_query_service is not None)
385
+ wait_hook = None
386
+ check_required = False
387
+ # In autocommit mode a failed statement cannot poison a transaction, and
388
+ # SAVEPOINT itself is rejected outside a transaction block
389
+ provenance_savepoint = False
390
+ try:
391
+ if not getattr(conn, "autocommit", False):
392
+ self.db_interface.execute_query_fetching_results(conn, "SAVEPOINT __jaaql_provenance")
393
+ provenance_savepoint = True
394
+ provenance_res = self.fetch_domain_types_via_provenance(conn, last_query, described, provenance)
395
+ if provenance_savepoint:
396
+ self.db_interface.execute_query_fetching_results(conn, "RELEASE SAVEPOINT __jaaql_provenance")
397
+ except Exception:
398
+ traceback.print_exc()
399
+ provenance_res = None
400
+ if provenance_savepoint:
401
+ self.db_interface.execute_query_fetching_results(conn, "ROLLBACK TO SAVEPOINT __jaaql_provenance")
402
+
403
+ if attempt_fetch_domain_types and provenance_res is None:
379
404
  temp_view_name = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
380
405
  last_query = "CREATE TEMP VIEW \"temp_view__" + temp_view_name + "\" AS (" + last_query + ")"
381
406
  self.db_interface.execute_query_fetching_results(conn, last_query, found_params, wait_hook=wait_hook,
@@ -436,23 +461,22 @@ WITH RECURSIVE column_hierarchy AS (
436
461
  AND u.view_schema = ch.table_schema
437
462
  AND u.view_name = ch.table_name
438
463
  AND u.column_name = ch.column_name
464
+ AND c2.udt_name = ch.udt_name
465
+ AND c2.domain_name IS NOT DISTINCT FROM ch.domain_name
439
466
  ORDER BY
440
- CASE WHEN ch.domain_name IS NOT DISTINCT FROM c2.domain_name THEN 3 ELSE 0 END DESC,
441
- CASE WHEN ch.udt_name = c2.udt_name THEN 2 ELSE 0 END DESC,
442
- CASE WHEN ch.data_type = c2.data_type THEN 1 ELSE 0 END DESC,
443
467
  c2.table_schema,
444
468
  c2.table_name
445
469
  LIMIT 1
446
470
  ) ref ON true
447
471
  ),
448
472
  deep AS (
449
- SELECT DISTINCT ON (column_name)
473
+ SELECT
450
474
  column_name,
451
475
  data_type,
452
476
  udt_name,
453
477
  domain_name
454
478
  FROM column_hierarchy
455
- ORDER BY column_name, level DESC
479
+ WHERE level = 1
456
480
  ),
457
481
  nul AS (
458
482
  SELECT
@@ -472,7 +496,7 @@ JOIN nul n USING (column_name)
472
496
  ORDER BY d.column_name;
473
497
  """
474
498
  found_params = {}
475
- elif do_prepare_only and not psql:
499
+ elif do_prepare_only and not attempt_fetch_domain_types and not psql:
476
500
  arg_open = "(" if len(found_params) != 0 else ""
477
501
  arg_close = ")" if len(found_params) != 0 else ""
478
502
  last_query = "PREPARE _jaaql_query_check_" + do_prepare_only + arg_open + ", ".join(
@@ -509,6 +533,8 @@ ORDER BY d.column_name;
509
533
  "rows": [result.stdout],
510
534
  "type_codes": [""]
511
535
  }
536
+ elif provenance_res is not None:
537
+ res = provenance_res
512
538
  else:
513
539
  res = self.db_interface.execute_query_fetching_results(conn, last_query, found_params, wait_hook=wait_hook,
514
540
  requires_dba_check=check_required and canned_query_service is not None,
@@ -518,7 +544,7 @@ ORDER BY d.column_name;
518
544
  self.db_interface.execute_query_fetching_results(conn, "DEALLOCATE _jaaql_query_check_" + do_prepare_only, found_params,
519
545
  wait_hook=wait_hook,
520
546
  requires_dba_check=check_required and canned_query_service is not None)
521
- elif attempt_fetch_domain_types:
547
+ elif attempt_fetch_domain_types and temp_view_name is not None:
522
548
  self.db_interface.execute_query_fetching_results(conn, "DROP VIEW IF EXISTS \"temp_view__" + temp_view_name + "\"", found_params,
523
549
  wait_hook=wait_hook,
524
550
  requires_dba_check=check_required and canned_query_service is not None)
@@ -835,6 +861,148 @@ ORDER BY d.column_name;
835
861
 
836
862
  return new_query + query[last_end_match:]
837
863
 
864
+ PROVENANCE__max_view_depth = 8
865
+
866
+ def describe_query_provenance(self, conn, query, parameters, wait_hook: queue.Queue = None, requires_dba_check: bool = False):
867
+ provenance = []
868
+ described = self.db_interface.execute_query_fetching_results(
869
+ conn, "SELECT * FROM (" + query + "\n) __jaaql_provenance LIMIT 0", parameters, wait_hook=wait_hook,
870
+ requires_dba_check=requires_dba_check, capture_provenance=provenance)
871
+ return described, provenance
872
+
873
+ def fetch_domain_types_via_provenance(self, conn, query, described, provenance):
874
+ null_extended, plan_cost = self.fetch_null_extended_relations(conn, query)
875
+ result_types = self.fetch_result_types_via_temp_view(conn, query)
876
+ if len(result_types) != len(provenance):
877
+ raise Exception("Temp view column count does not match the described result")
878
+ sources = self.resolve_column_provenance_batch(conn, [(table_oid, table_column) for _, table_oid, table_column in provenance])
879
+
880
+ rows = []
881
+ for column_name, (data_type, udt_name, domain_name), (type_oid, table_oid, table_column) in zip(
882
+ described["columns"], result_types, provenance):
883
+ source = sources.get((table_oid, table_column))
884
+ if source is None:
885
+ is_nullable = True
886
+ else:
887
+ nspname, relname, attnotnull = source
888
+ is_nullable = not attnotnull or (nspname, relname) in null_extended
889
+ rows.append([column_name, data_type, udt_name, domain_name, is_nullable])
890
+
891
+ rows.sort(key=lambda row: row[0])
892
+ return {
893
+ "columns": ["column_name", "data_type", "udt_name", "domain_name", "is_nullable"],
894
+ "rows": rows,
895
+ "type_codes": [],
896
+ "plan_cost": plan_cost
897
+ }
898
+
899
+ def fetch_result_types_via_temp_view(self, conn, query):
900
+ temp_view_name = "temp_view__" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
901
+ self.db_interface.execute_query_fetching_results(conn, "CREATE TEMP VIEW \"" + temp_view_name + "\" AS (" + query + ")")
902
+ try:
903
+ res = self.db_interface.execute_query_fetching_results(conn, """
904
+ SELECT format_type(COALESCE(bt.oid, t.oid), a.atttypmod) AS data_type,
905
+ COALESCE(bt.typname, t.typname) AS udt_name,
906
+ CASE WHEN t.typtype = 'd' THEN t.typname END AS domain_name
907
+ FROM pg_attribute a
908
+ JOIN pg_type t ON t.oid = a.atttypid
909
+ LEFT JOIN pg_type bt ON t.typtype = 'd' AND bt.oid = t.typbasetype
910
+ WHERE a.attrelid = '""" + temp_view_name + """'::regclass
911
+ AND a.attnum > 0
912
+ ORDER BY a.attnum""")
913
+ finally:
914
+ try:
915
+ self.db_interface.execute_query_fetching_results(conn, "DROP VIEW IF EXISTS \"" + temp_view_name + "\"")
916
+ except Exception:
917
+ pass
918
+ return res["rows"]
919
+
920
+ def resolve_column_provenance_batch(self, conn, pairs):
921
+ view_cache = {}
922
+ current = {}
923
+ results = {}
924
+ for table_oid, table_column in pairs:
925
+ if table_oid and table_column > 0:
926
+ current[(table_oid, table_column)] = (table_oid, table_column)
927
+ else:
928
+ results[(table_oid, table_column)] = None
929
+
930
+ for _ in range(self.PROVENANCE__max_view_depth + 1):
931
+ pending = {}
932
+ for origin, cursor in current.items():
933
+ if origin not in results:
934
+ pending.setdefault(cursor, []).append(origin)
935
+ if len(pending) == 0:
936
+ break
937
+ attributes = self.batch_attribute_lookup(conn, pending.keys())
938
+ for cursor, origins in pending.items():
939
+ attribute = attributes.get(cursor)
940
+ if attribute is None or attribute[4]:
941
+ for origin in origins:
942
+ results[origin] = None
943
+ continue
944
+ relkind, nspname, relname, attnotnull, attisdropped = attribute
945
+ if relkind != "v":
946
+ for origin in origins:
947
+ results[origin] = (nspname, relname, attnotnull)
948
+ continue
949
+ if cursor[0] not in view_cache:
950
+ viewdef = self.db_interface.execute_query_fetching_results(
951
+ conn, "SELECT pg_get_viewdef(" + str(int(cursor[0])) + ")")["rows"][0][0].strip().rstrip(";")
952
+ inner_provenance = []
953
+ self.db_interface.execute_query_fetching_results(
954
+ conn, "SELECT * FROM (" + viewdef + "\n) __jaaql_provenance LIMIT 0", capture_provenance=inner_provenance)
955
+ view_cache[cursor[0]] = inner_provenance
956
+ inner = view_cache[cursor[0]]
957
+ if cursor[1] > len(inner):
958
+ for origin in origins:
959
+ results[origin] = None
960
+ continue
961
+ _, inner_oid, inner_column = inner[cursor[1] - 1]
962
+ if inner_oid and inner_column > 0:
963
+ for origin in origins:
964
+ current[origin] = (inner_oid, inner_column)
965
+ else:
966
+ # The view column is an expression: its type is known but base nullability is not recoverable
967
+ for origin in origins:
968
+ results[origin] = (nspname, relname, False)
969
+
970
+ for origin in current:
971
+ results.setdefault(origin, None)
972
+ return results
973
+
974
+ def batch_attribute_lookup(self, conn, pairs):
975
+ values = ", ".join("(" + str(int(table_oid)) + ", " + str(int(table_column)) + ")" for table_oid, table_column in pairs)
976
+ res = self.db_interface.execute_query_fetching_results(conn, """
977
+ SELECT c.oid, a.attnum, c.relkind, n.nspname, c.relname, a.attnotnull, a.attisdropped
978
+ FROM pg_class c
979
+ JOIN pg_namespace n ON n.oid = c.relnamespace
980
+ JOIN pg_attribute a ON a.attrelid = c.oid
981
+ WHERE (c.oid, a.attnum) IN (""" + values + ")")
982
+ return {(row[0], row[1]): tuple(row[2:]) for row in res["rows"]}
983
+
984
+ def fetch_null_extended_relations(self, conn, query):
985
+ plan_payload = self.db_interface.execute_query_fetching_results(
986
+ conn, "EXPLAIN (VERBOSE, FORMAT JSON) " + query)["rows"][0][0]
987
+ if isinstance(plan_payload, str):
988
+ plan_payload = json.loads(plan_payload)
989
+ null_extended = set()
990
+
991
+ def walk(node, is_null_extended):
992
+ if node.get("Parent Relationship") in ("InitPlan", "SubPlan"):
993
+ return
994
+ if is_null_extended and "Relation Name" in node:
995
+ null_extended.add((node.get("Schema"), node["Relation Name"]))
996
+ join_type = node.get("Join Type")
997
+ for child_idx, child in enumerate(node.get("Plans", [])):
998
+ walk(child, is_null_extended
999
+ or (join_type == "Left" and child_idx == 1)
1000
+ or (join_type == "Right" and child_idx == 0)
1001
+ or join_type == "Full")
1002
+
1003
+ walk(plan_payload[0]["Plan"], False)
1004
+ return null_extended, plan_payload[0]["Plan"].get("Total Cost")
1005
+
838
1006
  def pre_prepare_statement(self, query, parameters, match_regex: str = REGEX_query_argument, encryption_key: bytes = None,
839
1007
  require_presence: bool = True, for_prepare: bool = False, skipped_singletons: list[str] = None,
840
1008
  replace_with_nulls: bool = False):
@@ -54,6 +54,8 @@ import requests
54
54
 
55
55
  from jaaql.migrations.migrations import run_migrations
56
56
 
57
+ PREPARE__concurrency = 6
58
+
57
59
  NGINX_MARKER__first = "limit_req_status 429;\n "
58
60
  NGINX_MARKER__second = "proxy_set_header X-Real-IP $remote_addr;\n "
59
61
  NGINX_INSERT__frozen = " return 503;\n "
@@ -398,28 +400,16 @@ ORDER BY
398
400
 
399
401
  self.is_dba(db_connection)
400
402
 
401
- res = []
402
-
403
- for query in inputs["queries"]:
403
+ def prepare_one(query):
404
404
  my_uuid = str(uuid.uuid4()).replace("-", "_")
405
405
  exc = None
406
406
  cost = None
407
407
  type_resolution_method = None
408
408
  columns = None
409
409
 
410
- try:
411
- results = execute_supplied_statement(
412
- db_connection,
413
- query["query"].strip(),
414
- do_prepare_only=my_uuid
415
- )
416
- cost = float(results["rows"][0][0].split("..")[1].split(" ")[0])
417
- except Exception as ex:
418
- exc = str(ex).replace("PREPARE _jaaql_query_check_" + my_uuid + " as ", "").replace(
419
- " ... _jaaql_query_check_" + my_uuid + " as ", "")
420
-
421
- if exc is None and not cost_only:
422
- # Try temp_view domain type resolution first
410
+ if not cost_only:
411
+ # Try provenance/temp_view domain type resolution first; its EXPLAIN also yields the
412
+ # plan cost, so a success skips the separate PREPARE validation pass entirely
423
413
  try:
424
414
  domain_types = execute_supplied_statement(
425
415
  db_connection,
@@ -428,6 +418,7 @@ ORDER BY
428
418
  attempt_fetch_domain_types=True
429
419
  )
430
420
  type_resolution_method = "temp_view"
421
+ cost = domain_types.get("plan_cost")
431
422
 
432
423
  temp_columns = [row[0] for row in domain_types['rows']]
433
424
 
@@ -456,8 +447,22 @@ ORDER BY
456
447
  except Exception:
457
448
  pass # This is fine
458
449
 
450
+ if columns is None and exc is None:
451
+ # Introspection unavailable (or cost_only): run the classic PREPARE pass for
452
+ # validation with canonical error messages, and the cost
453
+ try:
454
+ results = execute_supplied_statement(
455
+ db_connection,
456
+ query["query"].strip(),
457
+ do_prepare_only=my_uuid
458
+ )
459
+ cost = float(results["rows"][0][0].split("..")[1].split(" ")[0])
460
+ except Exception as ex:
461
+ exc = str(ex).replace("PREPARE _jaaql_query_check_" + my_uuid + " as ", "").replace(
462
+ " ... _jaaql_query_check_" + my_uuid + " as ", "")
463
+
459
464
  # If temp_view didn't work, fallback to gdesc/psql
460
- if columns is None and exc is None:
465
+ if exc is None and not cost_only:
461
466
  try:
462
467
  type_resolution_method = "gdesc"
463
468
  psql = [
@@ -483,14 +488,20 @@ ORDER BY
483
488
  type_resolution_method = "failed"
484
489
  columns = None
485
490
 
486
- res.append({
491
+ return {
487
492
  "location": query["file"] + ":" + str(query["line_number"]),
488
493
  "name": query["name"],
489
494
  "cost": cost,
490
495
  "columns": columns,
491
496
  "exception": exc,
492
497
  "type_resolution_method": type_resolution_method
493
- })
498
+ }
499
+
500
+ try:
501
+ from gevent.pool import Pool as GeventPool
502
+ res = list(GeventPool(PREPARE__concurrency).map(prepare_one, inputs["queries"]))
503
+ except ImportError:
504
+ res = [prepare_one(query) for query in inputs["queries"]]
494
505
 
495
506
  if inputs.get("sort_cost", True):
496
507
  return sorted(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaaql-middleware-python
3
- Version: 5.3.12
3
+ Version: 5.3.14
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv