jaaql-middleware-python 5.3.12__tar.gz → 5.3.13__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.13}/PKG-INFO +1 -1
  2. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/constants.py +1 -1
  3. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/db/db_interface.py +5 -3
  4. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/db/db_pg_interface.py +8 -1
  5. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/interpreter/interpret_jaaql.py +168 -7
  6. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/model.py +30 -19
  7. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql_middleware_python.egg-info/PKG-INFO +1 -1
  8. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/LICENSE.txt +0 -0
  9. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/README.md +0 -0
  10. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/__init__.py +0 -0
  11. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/config/__init__.py +0 -0
  12. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/config/config-docker.ini +0 -0
  13. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/config/config-test.ini +0 -0
  14. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/config/config.ini +0 -0
  15. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/config_constants.py +0 -0
  16. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/db/__init__.py +0 -0
  17. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/db/db_utils.py +0 -0
  18. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/db/db_utils_no_circ.py +0 -0
  19. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/documentation/__init__.py +0 -0
  20. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/documentation/documentation_internal.py +0 -0
  21. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/documentation/documentation_public.py +0 -0
  22. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/documentation/documentation_shared.py +0 -0
  23. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/email/__init__.py +0 -0
  24. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/email/email_manager.py +0 -0
  25. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/email/email_manager_service.py +0 -0
  26. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/email/patch_ems.py +0 -0
  27. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/exceptions/__init__.py +0 -0
  28. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/exceptions/custom_http_status.py +0 -0
  29. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/exceptions/http_status_exception.py +0 -0
  30. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/exceptions/jaaql_interpretable_handled_errors.py +0 -0
  31. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/exceptions/not_yet_implement_exception.py +0 -0
  32. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/generated_constants.py +0 -0
  33. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/interpreter/__init__.py +0 -0
  34. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/jaaql.py +0 -0
  35. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/migrations/__init__.py +0 -0
  36. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/migrations/migrations.py +0 -0
  37. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/__init__.py +0 -0
  38. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/base_controller.py +0 -0
  39. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/base_model.py +0 -0
  40. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/controller.py +0 -0
  41. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/controller_interface.py +0 -0
  42. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/exception_queries.py +0 -0
  43. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/generated_queries.py +0 -0
  44. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/handmade_queries.py +0 -0
  45. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/model_interface.py +0 -0
  46. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/mvc/response.py +0 -0
  47. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/openapi/__init__.py +0 -0
  48. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/openapi/swagger_documentation.py +0 -0
  49. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/patch.py +0 -0
  50. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/01.install_domains.generated.sql +0 -0
  51. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/02.install_super_user.exceptions.sql +0 -0
  52. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/03.install_super_user.handwritten.sql +0 -0
  53. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/04.install_jaaql_data_structures.generated.sql +0 -0
  54. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/05.install_static_data.generated.sql +0 -0
  55. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/06.install_jaaql.exceptions.sql +0 -0
  56. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/ZZZZ.generated_functions_views_and_permissions.sql +0 -0
  57. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/ZZZZ.reset_references.sql +0 -0
  58. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/scripts/swagger_template.html +0 -0
  59. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/services/__init__.py +0 -0
  60. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/services/cached_canned_query_service.py +0 -0
  61. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/services/migrations_manager_service.py +0 -0
  62. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/services/patch_mms.py +0 -0
  63. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/services/patch_shared_var_service.py +0 -0
  64. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/services/shared_var_service.py +0 -0
  65. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/__init__.py +0 -0
  66. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/bootstrap_secrets.py +0 -0
  67. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/cron.py +0 -0
  68. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/crypt_utils.py +0 -0
  69. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/options.py +0 -0
  70. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/utils.py +0 -0
  71. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/utils_no_project_imports.py +0 -0
  72. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql/utilities/vault.py +0 -0
  73. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql_middleware_python.egg-info/SOURCES.txt +0 -0
  74. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql_middleware_python.egg-info/dependency_links.txt +0 -0
  75. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql_middleware_python.egg-info/requires.txt +0 -0
  76. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/jaaql_middleware_python.egg-info/top_level.txt +0 -0
  77. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/setup.cfg +0 -0
  78. {jaaql_middleware_python-5.3.12 → jaaql_middleware_python-5.3.13}/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.13
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.13"
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,25 @@ 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
+ self.db_interface.execute_query_fetching_results(conn, "SAVEPOINT __jaaql_provenance")
388
+ try:
389
+ provenance_res = self.fetch_domain_types_via_provenance(conn, last_query, described, provenance)
390
+ self.db_interface.execute_query_fetching_results(conn, "RELEASE SAVEPOINT __jaaql_provenance")
391
+ except Exception:
392
+ traceback.print_exc()
393
+ provenance_res = None
394
+ self.db_interface.execute_query_fetching_results(conn, "ROLLBACK TO SAVEPOINT __jaaql_provenance")
395
+
396
+ if attempt_fetch_domain_types and provenance_res is None:
379
397
  temp_view_name = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
380
398
  last_query = "CREATE TEMP VIEW \"temp_view__" + temp_view_name + "\" AS (" + last_query + ")"
381
399
  self.db_interface.execute_query_fetching_results(conn, last_query, found_params, wait_hook=wait_hook,
@@ -436,23 +454,22 @@ WITH RECURSIVE column_hierarchy AS (
436
454
  AND u.view_schema = ch.table_schema
437
455
  AND u.view_name = ch.table_name
438
456
  AND u.column_name = ch.column_name
457
+ AND c2.udt_name = ch.udt_name
458
+ AND c2.domain_name IS NOT DISTINCT FROM ch.domain_name
439
459
  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
460
  c2.table_schema,
444
461
  c2.table_name
445
462
  LIMIT 1
446
463
  ) ref ON true
447
464
  ),
448
465
  deep AS (
449
- SELECT DISTINCT ON (column_name)
466
+ SELECT
450
467
  column_name,
451
468
  data_type,
452
469
  udt_name,
453
470
  domain_name
454
471
  FROM column_hierarchy
455
- ORDER BY column_name, level DESC
472
+ WHERE level = 1
456
473
  ),
457
474
  nul AS (
458
475
  SELECT
@@ -472,7 +489,7 @@ JOIN nul n USING (column_name)
472
489
  ORDER BY d.column_name;
473
490
  """
474
491
  found_params = {}
475
- elif do_prepare_only and not psql:
492
+ elif do_prepare_only and not attempt_fetch_domain_types and not psql:
476
493
  arg_open = "(" if len(found_params) != 0 else ""
477
494
  arg_close = ")" if len(found_params) != 0 else ""
478
495
  last_query = "PREPARE _jaaql_query_check_" + do_prepare_only + arg_open + ", ".join(
@@ -509,6 +526,8 @@ ORDER BY d.column_name;
509
526
  "rows": [result.stdout],
510
527
  "type_codes": [""]
511
528
  }
529
+ elif provenance_res is not None:
530
+ res = provenance_res
512
531
  else:
513
532
  res = self.db_interface.execute_query_fetching_results(conn, last_query, found_params, wait_hook=wait_hook,
514
533
  requires_dba_check=check_required and canned_query_service is not None,
@@ -518,7 +537,7 @@ ORDER BY d.column_name;
518
537
  self.db_interface.execute_query_fetching_results(conn, "DEALLOCATE _jaaql_query_check_" + do_prepare_only, found_params,
519
538
  wait_hook=wait_hook,
520
539
  requires_dba_check=check_required and canned_query_service is not None)
521
- elif attempt_fetch_domain_types:
540
+ elif attempt_fetch_domain_types and temp_view_name is not None:
522
541
  self.db_interface.execute_query_fetching_results(conn, "DROP VIEW IF EXISTS \"temp_view__" + temp_view_name + "\"", found_params,
523
542
  wait_hook=wait_hook,
524
543
  requires_dba_check=check_required and canned_query_service is not None)
@@ -835,6 +854,148 @@ ORDER BY d.column_name;
835
854
 
836
855
  return new_query + query[last_end_match:]
837
856
 
857
+ PROVENANCE__max_view_depth = 8
858
+
859
+ def describe_query_provenance(self, conn, query, parameters, wait_hook: queue.Queue = None, requires_dba_check: bool = False):
860
+ provenance = []
861
+ described = self.db_interface.execute_query_fetching_results(
862
+ conn, "SELECT * FROM (" + query + "\n) __jaaql_provenance LIMIT 0", parameters, wait_hook=wait_hook,
863
+ requires_dba_check=requires_dba_check, capture_provenance=provenance)
864
+ return described, provenance
865
+
866
+ def fetch_domain_types_via_provenance(self, conn, query, described, provenance):
867
+ null_extended, plan_cost = self.fetch_null_extended_relations(conn, query)
868
+ result_types = self.fetch_result_types_via_temp_view(conn, query)
869
+ if len(result_types) != len(provenance):
870
+ raise Exception("Temp view column count does not match the described result")
871
+ sources = self.resolve_column_provenance_batch(conn, [(table_oid, table_column) for _, table_oid, table_column in provenance])
872
+
873
+ rows = []
874
+ for column_name, (data_type, udt_name, domain_name), (type_oid, table_oid, table_column) in zip(
875
+ described["columns"], result_types, provenance):
876
+ source = sources.get((table_oid, table_column))
877
+ if source is None:
878
+ is_nullable = True
879
+ else:
880
+ nspname, relname, attnotnull = source
881
+ is_nullable = not attnotnull or (nspname, relname) in null_extended
882
+ rows.append([column_name, data_type, udt_name, domain_name, is_nullable])
883
+
884
+ rows.sort(key=lambda row: row[0])
885
+ return {
886
+ "columns": ["column_name", "data_type", "udt_name", "domain_name", "is_nullable"],
887
+ "rows": rows,
888
+ "type_codes": [],
889
+ "plan_cost": plan_cost
890
+ }
891
+
892
+ def fetch_result_types_via_temp_view(self, conn, query):
893
+ temp_view_name = "temp_view__" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
894
+ self.db_interface.execute_query_fetching_results(conn, "CREATE TEMP VIEW \"" + temp_view_name + "\" AS (" + query + ")")
895
+ try:
896
+ res = self.db_interface.execute_query_fetching_results(conn, """
897
+ SELECT format_type(COALESCE(bt.oid, t.oid), a.atttypmod) AS data_type,
898
+ COALESCE(bt.typname, t.typname) AS udt_name,
899
+ CASE WHEN t.typtype = 'd' THEN t.typname END AS domain_name
900
+ FROM pg_attribute a
901
+ JOIN pg_type t ON t.oid = a.atttypid
902
+ LEFT JOIN pg_type bt ON t.typtype = 'd' AND bt.oid = t.typbasetype
903
+ WHERE a.attrelid = '""" + temp_view_name + """'::regclass
904
+ AND a.attnum > 0
905
+ ORDER BY a.attnum""")
906
+ finally:
907
+ try:
908
+ self.db_interface.execute_query_fetching_results(conn, "DROP VIEW IF EXISTS \"" + temp_view_name + "\"")
909
+ except Exception:
910
+ pass
911
+ return res["rows"]
912
+
913
+ def resolve_column_provenance_batch(self, conn, pairs):
914
+ view_cache = {}
915
+ current = {}
916
+ results = {}
917
+ for table_oid, table_column in pairs:
918
+ if table_oid and table_column > 0:
919
+ current[(table_oid, table_column)] = (table_oid, table_column)
920
+ else:
921
+ results[(table_oid, table_column)] = None
922
+
923
+ for _ in range(self.PROVENANCE__max_view_depth + 1):
924
+ pending = {}
925
+ for origin, cursor in current.items():
926
+ if origin not in results:
927
+ pending.setdefault(cursor, []).append(origin)
928
+ if len(pending) == 0:
929
+ break
930
+ attributes = self.batch_attribute_lookup(conn, pending.keys())
931
+ for cursor, origins in pending.items():
932
+ attribute = attributes.get(cursor)
933
+ if attribute is None or attribute[4]:
934
+ for origin in origins:
935
+ results[origin] = None
936
+ continue
937
+ relkind, nspname, relname, attnotnull, attisdropped = attribute
938
+ if relkind != "v":
939
+ for origin in origins:
940
+ results[origin] = (nspname, relname, attnotnull)
941
+ continue
942
+ if cursor[0] not in view_cache:
943
+ viewdef = self.db_interface.execute_query_fetching_results(
944
+ conn, "SELECT pg_get_viewdef(" + str(int(cursor[0])) + ")")["rows"][0][0].strip().rstrip(";")
945
+ inner_provenance = []
946
+ self.db_interface.execute_query_fetching_results(
947
+ conn, "SELECT * FROM (" + viewdef + "\n) __jaaql_provenance LIMIT 0", capture_provenance=inner_provenance)
948
+ view_cache[cursor[0]] = inner_provenance
949
+ inner = view_cache[cursor[0]]
950
+ if cursor[1] > len(inner):
951
+ for origin in origins:
952
+ results[origin] = None
953
+ continue
954
+ _, inner_oid, inner_column = inner[cursor[1] - 1]
955
+ if inner_oid and inner_column > 0:
956
+ for origin in origins:
957
+ current[origin] = (inner_oid, inner_column)
958
+ else:
959
+ # The view column is an expression: its type is known but base nullability is not recoverable
960
+ for origin in origins:
961
+ results[origin] = (nspname, relname, False)
962
+
963
+ for origin in current:
964
+ results.setdefault(origin, None)
965
+ return results
966
+
967
+ def batch_attribute_lookup(self, conn, pairs):
968
+ values = ", ".join("(" + str(int(table_oid)) + ", " + str(int(table_column)) + ")" for table_oid, table_column in pairs)
969
+ res = self.db_interface.execute_query_fetching_results(conn, """
970
+ SELECT c.oid, a.attnum, c.relkind, n.nspname, c.relname, a.attnotnull, a.attisdropped
971
+ FROM pg_class c
972
+ JOIN pg_namespace n ON n.oid = c.relnamespace
973
+ JOIN pg_attribute a ON a.attrelid = c.oid
974
+ WHERE (c.oid, a.attnum) IN (""" + values + ")")
975
+ return {(row[0], row[1]): tuple(row[2:]) for row in res["rows"]}
976
+
977
+ def fetch_null_extended_relations(self, conn, query):
978
+ plan_payload = self.db_interface.execute_query_fetching_results(
979
+ conn, "EXPLAIN (VERBOSE, FORMAT JSON) " + query)["rows"][0][0]
980
+ if isinstance(plan_payload, str):
981
+ plan_payload = json.loads(plan_payload)
982
+ null_extended = set()
983
+
984
+ def walk(node, is_null_extended):
985
+ if node.get("Parent Relationship") in ("InitPlan", "SubPlan"):
986
+ return
987
+ if is_null_extended and "Relation Name" in node:
988
+ null_extended.add((node.get("Schema"), node["Relation Name"]))
989
+ join_type = node.get("Join Type")
990
+ for child_idx, child in enumerate(node.get("Plans", [])):
991
+ walk(child, is_null_extended
992
+ or (join_type == "Left" and child_idx == 1)
993
+ or (join_type == "Right" and child_idx == 0)
994
+ or join_type == "Full")
995
+
996
+ walk(plan_payload[0]["Plan"], False)
997
+ return null_extended, plan_payload[0]["Plan"].get("Total Cost")
998
+
838
999
  def pre_prepare_statement(self, query, parameters, match_regex: str = REGEX_query_argument, encryption_key: bytes = None,
839
1000
  require_presence: bool = True, for_prepare: bool = False, skipped_singletons: list[str] = None,
840
1001
  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.13
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