iris-pgwire 1.5.9__tar.gz → 1.7.1__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 (133) hide show
  1. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/.gitignore +5 -1
  2. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/PKG-INFO +4 -4
  3. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/pyproject.toml +10 -2
  4. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/__init__.py +1 -1
  5. iris_pgwire-1.7.1/src/iris_pgwire/catalog/_reentrancy.py +27 -0
  6. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/catalog_router.py +158 -15
  7. iris_pgwire-1.7.1/src/iris_pgwire/catalog/function_installer.py +103 -0
  8. iris_pgwire-1.7.1/src/iris_pgwire/catalog/functions.py +566 -0
  9. iris_pgwire-1.7.1/src/iris_pgwire/catalog/views/__init__.py +18 -0
  10. iris_pgwire-1.7.1/src/iris_pgwire/catalog/views/definitions.py +731 -0
  11. iris_pgwire-1.7.1/src/iris_pgwire/catalog/views/installer.py +98 -0
  12. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/dbapi_executor.py +153 -4
  13. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_executor.py +335 -29
  14. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/protocol.py +79 -21
  15. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/schema_mapper.py +59 -1
  16. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/server.py +46 -0
  17. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/array_literal.py +71 -0
  18. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/array_params.py +287 -0
  19. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/boolean_expr.py +355 -0
  20. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/column_types.py +197 -0
  21. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/normalizer.py +78 -0
  22. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/pg_array.py +99 -0
  23. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/pg_functions.py +137 -0
  24. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/refiner.py +25 -8
  25. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/sqlstate.py +177 -0
  26. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/statement_filter.py +10 -0
  27. iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/verbatim.py +49 -0
  28. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/LICENSE +0 -0
  29. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/README.md +0 -0
  30. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_column_naming.py +0 -0
  31. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_noop_cursor.py +0 -0
  32. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_result_processing.py +0 -0
  33. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_type_mapping.py +0 -0
  34. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/__init__.py +0 -0
  35. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/auth_selector.py +0 -0
  36. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/gssapi_auth.py +0 -0
  37. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/oauth_bridge.py +0 -0
  38. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/scram.py +0 -0
  39. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/wallet_credentials.py +0 -0
  40. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/backend_selector.py +0 -0
  41. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/bulk_executor.py +0 -0
  42. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/__init__.py +0 -0
  43. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/catalog_functions.py +0 -0
  44. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/oid_generator.py +0 -0
  45. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_attrdef.py +0 -0
  46. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
  47. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_class.py +0 -0
  48. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
  49. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_index.py +0 -0
  50. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
  51. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_type.py +0 -0
  52. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/column_validator.py +0 -0
  53. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/config.py +0 -0
  54. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/config_schema.py +0 -0
  55. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/constitutional.py +0 -0
  56. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/__init__.py +0 -0
  57. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
  58. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/date_horolog.py +0 -0
  59. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
  60. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/ddl_splitter.py +0 -0
  61. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/json_path.py +0 -0
  62. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
  63. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/copy_handler.py +0 -0
  64. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/csv_processor.py +0 -0
  65. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/dbapi_connection_pool.py +0 -0
  66. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/debug_tracer.py +0 -0
  67. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/health_checker.py +0 -0
  68. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/integratedml.py +0 -0
  69. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_constructs.py +0 -0
  70. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_log_handler.py +0 -0
  71. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_user_management.py +0 -0
  72. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/migrations/__init__.py +0 -0
  73. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/migrations/__main__.py +0 -0
  74. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/migrations/executor.py +0 -0
  75. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/__init__.py +0 -0
  76. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/backend_config.py +0 -0
  77. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/connection_pool_state.py +0 -0
  78. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/dbapi_connection.py +0 -0
  79. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/ipm_metadata.py +0 -0
  80. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/vector_query_request.py +0 -0
  81. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/observability.py +0 -0
  82. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/performance_monitor.py +0 -0
  83. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/__init__.py +0 -0
  84. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/__main__.py +0 -0
  85. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
  86. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/documentation_validator.py +0 -0
  87. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
  88. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/security_validator.py +0 -0
  89. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/validator.py +0 -0
  90. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/__init__.py +0 -0
  91. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
  92. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/api.py +0 -0
  93. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
  94. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/cache.py +0 -0
  95. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/confidence_analyzer.py +0 -0
  96. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/config.py +0 -0
  97. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
  98. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
  99. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
  100. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/ddl_parser.py +0 -0
  101. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/ddl_translator.py +0 -0
  102. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/debug.py +0 -0
  103. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/default_values.py +0 -0
  104. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
  105. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
  106. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/error_handler.py +0 -0
  107. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/identifier_normalizer.py +0 -0
  108. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/index_validator.py +0 -0
  109. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
  110. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
  111. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
  112. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
  113. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
  114. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
  115. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
  116. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
  117. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/metrics.py +0 -0
  118. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/models.py +0 -0
  119. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/parser.py +0 -0
  120. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/performance_monitor.py +0 -0
  121. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
  122. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
  123. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
  124. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
  125. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
  126. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/translator.py +0 -0
  127. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/type_translator.py +0 -0
  128. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/validator.py +0 -0
  129. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/testing/__init__.py +0 -0
  130. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
  131. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/type_mapping.py +0 -0
  132. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/vector_metrics.py +0 -0
  133. {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/vector_optimizer.py +0 -0
@@ -94,7 +94,6 @@ merge.cpf
94
94
  iris_pgwire.json
95
95
 
96
96
  # AI tooling files (not for public repo)
97
- AGENTS.md
98
97
  CODEX.md
99
98
  TABNINE.md
100
99
 
@@ -111,3 +110,8 @@ test_pg_type.py
111
110
  test_scope.py
112
111
  update_article.py
113
112
  verify_demo.sql
113
+
114
+ # iris-agentic-dev local connection config (per-developer: host, port, container
115
+ # name and credentials vary by machine). Template is committed at
116
+ # specs/043-local-first-sync/spikes/iris-agentic-dev.toml.example
117
+ .iris-agentic-dev.toml
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: iris-pgwire
3
- Version: 1.5.9
3
+ Version: 1.7.1
4
4
  Summary: PostgreSQL Wire Protocol Server for InterSystems IRIS - Connect BI tools, Python frameworks, and PostgreSQL clients to IRIS databases
5
5
  Project-URL: Homepage, https://github.com/intersystems-community/iris-pgwire
6
6
  Project-URL: Documentation, https://github.com/intersystems-community/iris-pgwire#readme
@@ -57,12 +57,12 @@ Requires-Dist: opentelemetry-instrumentation-asyncio>=0.41b0
57
57
  Requires-Dist: opentelemetry-sdk>=1.20.0
58
58
  Requires-Dist: prometheus-client>=0.17.0
59
59
  Requires-Dist: psycopg2-binary>=2.9.10
60
- Requires-Dist: pydantic>=2.0.0
60
+ Requires-Dist: pydantic<2.12,>=2.0.0
61
61
  Requires-Dist: pyyaml>=6.0.0
62
62
  Requires-Dist: sqlparse>=0.4.0
63
63
  Requires-Dist: structlog>=23.0.0
64
+ Requires-Dist: typing-extensions<5,>=4.12.2
64
65
  Provides-Extra: ai
65
- Requires-Dist: iris-agentic-dev>=1.0; extra == 'ai'
66
66
  Provides-Extra: dev
67
67
  Requires-Dist: black>=23.0.0; extra == 'dev'
68
68
  Requires-Dist: mypy>=1.5.0; extra == 'dev'
@@ -63,7 +63,9 @@ dependencies = [
63
63
  "opentelemetry-sdk>=1.20.0",
64
64
  "opentelemetry-instrumentation-asyncio>=0.41b0",
65
65
  "opentelemetry-exporter-otlp>=1.20.0",
66
- "pydantic>=2.0.0",
66
+ # pydantic>=2.12 requires typing_extensions>=4.14.1; IRIS irispython ships 4.12.2
67
+ "pydantic>=2.0.0,<2.12",
68
+ "typing_extensions>=4.12.2,<5",
67
69
  "pyyaml>=6.0.0",
68
70
  "prometheus-client>=0.17.0",
69
71
  ]
@@ -100,7 +102,13 @@ iris = [
100
102
  # "intersystems-iris>=3.6.0",
101
103
  ]
102
104
  ai = [
103
- "iris-agentic-dev>=1.0",
105
+ # IRIS-aware agent tooling. iris-agentic-dev is a Rust binary and is NOT
106
+ # published on PyPI, so it cannot be declared as a dependency here — doing
107
+ # so made `pip install iris-pgwire[ai]` fail outright. Install it directly:
108
+ # Linux: curl -fsSL https://github.com/intersystems-community/iris-agentic-dev/releases/latest/download/iris-agentic-dev-linux-x86_64 \
109
+ # -o /usr/local/bin/iris-agentic-dev && chmod +x /usr/local/bin/iris-agentic-dev
110
+ # macOS: brew install https://raw.githubusercontent.com/intersystems-community/iris-agentic-dev/master/Formula/iris-agentic-dev.rb
111
+ # See docs/iris-pgwire-iad-setup-issues.md and AGENTS.md.
104
112
  ]
105
113
  kerberos = [
106
114
  # Kerberos GSSAPI authentication (optional - not implemented in wire protocol yet)
@@ -6,7 +6,7 @@ Based on the specification from docs/iris_pgwire_plan.md and proven patterns fro
6
6
  caretdev/sqlalchemy-iris.
7
7
  """
8
8
 
9
- __version__ = "1.5.9"
9
+ __version__ = "1.7.1"
10
10
  __author__ = "Thomas Dyar <thomas.dyar@intersystems.com>"
11
11
 
12
12
  # Don't import server/protocol in __init__ to avoid sys.modules conflicts
@@ -0,0 +1,27 @@
1
+ """Re-entrancy guard for catalog query handling.
2
+
3
+ Lives in its own module because two things need it and importing it from
4
+ `catalog_router` creates a cycle: the router imports the view registry, and the
5
+ view installer needs the guard.
6
+
7
+ Some handlers answer a catalog query by issuing their own SQL through the
8
+ executor — `_build_pg_class_response` asks IRIS for `INFORMATION_SCHEMA.TABLES`.
9
+ That inner query re-enters the router, which would intercept it and return zero
10
+ rows, leaving the handler with nothing to report. The view installer has the
11
+ same problem in a sharper form: its `CREATE VIEW pg_catalog.pg_class AS ...` was
12
+ handed to the pg_class handler, which answered it with a synthetic success — so
13
+ installation reported success while creating nothing.
14
+
15
+ A ContextVar rather than a flag, so the guard is scoped to the asyncio task and
16
+ one session's internal query cannot suppress interception for a concurrent one.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import contextvars
22
+
23
+ _IN_CATALOG_HANDLER: contextvars.ContextVar[bool] = contextvars.ContextVar(
24
+ "iris_pgwire_in_catalog_handler", default=False
25
+ )
26
+
27
+ __all__ = ["_IN_CATALOG_HANDLER"]
@@ -15,6 +15,7 @@ import structlog
15
15
  from iris_pgwire.schema_mapper import IRIS_SCHEMA
16
16
  from iris_pgwire.type_mapping import get_type_mapping
17
17
 
18
+ from ._reentrancy import _IN_CATALOG_HANDLER
18
19
  from .oid_generator import OIDGenerator
19
20
  from .pg_attrdef import PgAttrdefEmulator
20
21
  from .pg_attribute import PgAttributeEmulator
@@ -23,10 +24,12 @@ from .pg_constraint import PgConstraintEmulator
23
24
  from .pg_index import PgIndexEmulator
24
25
  from .pg_namespace import PgNamespaceEmulator
25
26
  from .pg_type import PgTypeEmulator
27
+ from .views.definitions import CATALOG_SCHEMA, VIEW_BACKED_TABLES
26
28
 
27
29
  logger = structlog.get_logger(__name__)
28
30
 
29
31
 
32
+
30
33
  @dataclass
31
34
  class CatalogQueryResult:
32
35
  """
@@ -138,10 +141,8 @@ class CatalogRouter:
138
141
  "pg_namespace": self._handle_pg_namespace,
139
142
  "pg_class": self._handle_pg_class,
140
143
  "information_schema.columns": self._handle_information_schema_columns,
141
- "pg_attribute": self._handle_pg_attribute,
142
144
  "pg_constraint": self._handle_pg_constraint,
143
145
  "pg_index": self._handle_pg_index,
144
- "pg_attrdef": self._handle_pg_attrdef,
145
146
  }
146
147
 
147
148
  def can_handle(self, query: str) -> bool:
@@ -174,12 +175,20 @@ class CatalogRouter:
174
175
  Returns:
175
176
  Set of catalog table names (lowercase)
176
177
  """
177
- words = set(re.findall(r"\b(\w+)\b", query.lower()))
178
+ lowered = query.lower()
179
+ words = set(re.findall(r"\b(\w+)\b", lowered))
180
+ # A pg_* name followed by `(` is a function call, not a table. Without
181
+ # this, `pg_get_constraintdef(constr.oid)` counted as a catalog table, so
182
+ # the set of targeted tables was never a subset of the view-backed ones
183
+ # and the decline below never fired — the pg_class handler then answered
184
+ # Prisma's constraints query with pg_class's own 32 columns, including
185
+ # relfrozenxid typed `xid`. Measured: that is the whole of the failure.
186
+ called = set(re.findall(r"\b(pg_\w+)\s*\(", lowered))
178
187
  tables: set[str] = set()
179
188
 
180
189
  # Find pg_* references
181
190
  for word in words:
182
- if word.startswith("pg_"):
191
+ if word.startswith("pg_") and word not in called:
183
192
  if word in self.CATALOG_TABLES or len(word) > 3:
184
193
  tables.add(word)
185
194
 
@@ -298,6 +307,32 @@ class CatalogRouter:
298
307
 
299
308
  return self._regclass_pattern.sub(replace_regclass, query)
300
309
 
310
+ # The relation a statement is about: the first catalog table after FROM, or
311
+ # after a JOIN only if no FROM relation is a catalog table at all. Optional
312
+ # schema qualifier, optional quotes, optional alias.
313
+ _FROM_RELATION = re.compile(
314
+ r"\bFROM\s+(?:(?:\"?\w+\"?)\s*\.\s*)?\"?(\w+)\"?",
315
+ re.IGNORECASE,
316
+ )
317
+
318
+ def primary_catalog_relation(self, query: str) -> str | None:
319
+ """The catalog table the query is *about*, or None if it is about none.
320
+
321
+ Distinct from :meth:`get_target_catalog`, which returns the highest-
322
+ priority catalog table mentioned *anywhere*. That is the right question
323
+ for "which emulator knows most about this statement" and the wrong one
324
+ for "which table's columns should the answer have": Prisma's pg_views
325
+ query mentions pg_class in a JOIN, and answering with pg_class's shape
326
+ made the client fail (T015a).
327
+ """
328
+ for match in self._FROM_RELATION.finditer(query):
329
+ name = match.group(1).lower()
330
+ if name in self.CATALOG_TABLES or (
331
+ name.startswith("pg_") and len(name) > 3
332
+ ):
333
+ return name
334
+ return None
335
+
301
336
  def get_target_catalog(self, query: str) -> str | None:
302
337
  """
303
338
  Get primary catalog table targeted by query.
@@ -330,6 +365,17 @@ class CatalogRouter:
330
365
  # Return first found
331
366
  return next(iter(tables))
332
367
 
368
+ # Expressions the row emulators cannot evaluate. They can project and
369
+ # filter columns over emulated rows; they cannot compute EXISTS(...) or
370
+ # call scalar functions. Queries containing these must fall through to
371
+ # SQLInterceptor, which has purpose-built handlers for them — notably
372
+ # Prisma's schema probe:
373
+ # SELECT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = $1),
374
+ # version(), current_setting('server_version_num')
375
+ # Intercepting that returned raw namespace rows, so Prisma read it as
376
+ # "public does not exist" and issued CREATE SCHEMA "public".
377
+ UNEVALUABLE_EXPRESSIONS = ("EXISTS(", "EXISTS (", "VERSION()", "CURRENT_SETTING(")
378
+
333
379
  async def handle_catalog_query(
334
380
  self,
335
381
  sql: str,
@@ -349,20 +395,109 @@ class CatalogRouter:
349
395
  session_id=session_id,
350
396
  )
351
397
 
398
+ # Tables served by real IRIS views are answered by the database, which
399
+ # evaluates projections, aliases, joins and CTEs correctly. Declining
400
+ # here is what makes exactly one path answer a given catalog table
401
+ # (spec 044 FR-011). Without it the old handler would intercept first
402
+ # and the view would be unreachable.
403
+ # extract_catalog_tables() matches anything with a pg_ prefix, so the
404
+ # schema qualifier in "pg_catalog.pg_class" comes back looking like a
405
+ # table. Drop it before deciding, or no qualified query would ever be
406
+ # declined.
407
+ targeted = {t for t in self.extract_catalog_tables(sql) if t != CATALOG_SCHEMA}
408
+ if targeted and targeted <= set(VIEW_BACKED_TABLES):
409
+ logger.debug(
410
+ "Declining catalog query served by IRIS views",
411
+ tables=sorted(targeted),
412
+ session_id=session_id,
413
+ )
414
+ return None
415
+
416
+ if _IN_CATALOG_HANDLER.get():
417
+ logger.debug(
418
+ "Declining nested catalog query issued by a handler",
419
+ sql_preview=sql[:120],
420
+ session_id=session_id,
421
+ )
422
+ return None
423
+
424
+ if any(token in sql_upper for token in self.UNEVALUABLE_EXPRESSIONS):
425
+ logger.debug(
426
+ "Declining catalog query with unevaluable expression",
427
+ sql_preview=sql[:120],
428
+ session_id=session_id,
429
+ )
430
+ return None
431
+
352
432
  tables = self.extract_catalog_tables(sql)
353
- for table in self.CATALOG_HANDLER_PRIORITY:
354
- if table not in tables:
355
- continue
356
- handler = self._catalog_handler_map.get(table)
357
- if not handler:
358
- continue
359
- result = await handler(sql, sql_upper, params, session_id, executor)
360
- if result is not None:
361
- return result
433
+
434
+ # A handler may only answer for the relation the query is *about*.
435
+ #
436
+ # The loop below walks a priority list and takes the first handler whose
437
+ # table appears anywhere in the statement — a JOIN partner included. So a
438
+ # question about pg_views, which names pg_class only in a JOIN, was
439
+ # answered by the pg_class handler with pg_class's own 32 columns, and
440
+ # the client failed on `relfrozenxid` typed `xid`. The same happened to
441
+ # Prisma's constraints query before pg_constraint became a view, and
442
+ # would happen to the next catalog table with no path of its own.
443
+ #
444
+ # Declining is the honest alternative: IRIS then reports the relation it
445
+ # cannot find, which reaches the client as 42P01 (spec FR-008a). What is
446
+ # never acceptable is a confidently wrong shape.
447
+ # Scoped deliberately to the *substitution* case: the primary relation has
448
+ # no handler, but some other table in the statement does, so falling
449
+ # through would answer with that other table's columns. A statement whose
450
+ # only catalog table is unhandled still reaches the "empty fallback"
451
+ # below — that fabricated empty result is a separate defect (FR-008c),
452
+ # already located, and removing it is T020's own verification pass rather
453
+ # than something to smuggle in here.
454
+ primary = self.primary_catalog_relation(sql)
455
+ substitutes = primary is not None and primary not in self._catalog_handler_map
456
+ if substitutes and any(
457
+ table != primary and table in self._catalog_handler_map for table in tables
458
+ ):
459
+ logger.debug(
460
+ "Declining: would answer with another relation's columns",
461
+ primary=primary,
462
+ targeted=sorted(tables),
463
+ session_id=session_id,
464
+ )
465
+ return None
466
+
467
+ guard_token = _IN_CATALOG_HANDLER.set(True)
468
+ try:
469
+ for table in self.CATALOG_HANDLER_PRIORITY:
470
+ if table not in tables:
471
+ continue
472
+ # Same rule inside the loop: only the primary relation's handler
473
+ # may answer, so priority order cannot substitute a JOIN partner.
474
+ if primary is not None and table != primary:
475
+ continue
476
+ handler = self._catalog_handler_map.get(table)
477
+ if not handler:
478
+ continue
479
+ result = await handler(sql, sql_upper, params, session_id, executor)
480
+ if result is not None:
481
+ return result
482
+ finally:
483
+ _IN_CATALOG_HANDLER.reset(guard_token)
362
484
 
363
485
  if self.can_handle(sql):
364
486
  target = self.get_target_catalog(sql)
365
- logger.info(f"Intercepting {target} query (empty fallback)", session_id=session_id)
487
+ # FR-008c forbids this branch: it fabricates a zero-row, zero-column
488
+ # answer for any catalog query can_handle() claims but no handler
489
+ # recognises, so a client cannot tell an empty schema from an
490
+ # unimplemented one. T020 removes it. Until then the log has to name
491
+ # the statement, or the only trace of a swallowed query is the target
492
+ # table it was misattributed to — which is how four of Prisma's
493
+ # twelve statements went unnoticed.
494
+ logger.warning(
495
+ "Fabricating empty catalog result (FR-008c violation, T020)",
496
+ target=target,
497
+ primary_relation=self.primary_catalog_relation(sql),
498
+ sql_preview=" ".join(sql.split())[:220],
499
+ session_id=session_id,
500
+ )
366
501
  return {
367
502
  "success": True,
368
503
  "rows": [],
@@ -494,7 +629,14 @@ class CatalogRouter:
494
629
  ) -> dict[str, Any] | None:
495
630
  is_simple_pg_namespace = (
496
631
  "PG_NAMESPACE" in sql_upper
497
- and re.search(r"\bFROM\s+PG_NAMESPACE\b", sql_upper)
632
+ # Accept the schema-qualified form too. Real clients emit
633
+ # "FROM pg_catalog.pg_namespace"; requiring the bare name sent every
634
+ # qualified query to the empty fallback, which returns zero rows.
635
+ # Prisma's schema-existence probe is
636
+ # SELECT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = $1)
637
+ # so an empty result reads as "the public schema does not exist" and
638
+ # introspection reports the database as empty.
639
+ and re.search(r"\bFROM\s+(?:PG_CATALOG\.)?PG_NAMESPACE\b", sql_upper)
498
640
  and "JOIN" not in sql_upper
499
641
  and len(re.findall(r"\bFROM\b", sql_upper)) <= 2
500
642
  )
@@ -533,6 +675,7 @@ class CatalogRouter:
533
675
  and "PG_ATTRIBUTE" not in sql_upper
534
676
  and "ATT.ATTTYPID" not in sql_upper
535
677
  and "INFO.COLUMN_NAME" not in sql_upper
678
+ and not sql_upper.lstrip().startswith("WITH")
536
679
  )
537
680
 
538
681
  if not is_simple_pg_class:
@@ -0,0 +1,103 @@
1
+ """Create the PGWire SQL functions in IRIS, idempotently.
2
+
3
+ Runs before the view installer, which cannot define a single view without them.
4
+
5
+ Before this existed the functions had to be loaded by hand with
6
+ `$SYSTEM.OBJ.Load`, and nothing in the codebase did it — so catalog views only
7
+ worked on an instance where someone had already done so, and a fresh one would
8
+ fail at startup. Installing them over SQL removes the manual step and works
9
+ identically on both backends, since `CREATE FUNCTION` needs only a connection
10
+ whereas loading a class file needs the source on the server's own filesystem.
11
+
12
+ Failures are raised, never swallowed, for the same reason the view installer
13
+ raises: a catalog that half-installs answers introspection with empty results,
14
+ which reads to a client as "this database has nothing in it" (spec FR-009).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import structlog
20
+
21
+ from ..sql_translator.verbatim import verbatim_sql
22
+ from ._reentrancy import _IN_CATALOG_HANDLER
23
+ from .functions import CATALOG_FUNCTIONS, CATALOG_SCHEMA, CatalogFunction
24
+
25
+ logger = structlog.get_logger(__name__)
26
+
27
+
28
+ class CatalogFunctionInstallError(RuntimeError):
29
+ """Raised when a PGWire SQL function cannot be created."""
30
+
31
+
32
+ class CatalogFunctionInstaller:
33
+ """Install the ObjectScript-bodied SQL functions the catalog depends on."""
34
+
35
+ def __init__(self, executor):
36
+ self._executor = executor
37
+
38
+ async def install(self, session_id: str | None = None) -> list[str]:
39
+ """Create every function, replacing any existing definition.
40
+
41
+ Returns the qualified names installed. Idempotent by construction —
42
+ `CREATE OR REPLACE` converges rather than needing a DROP first.
43
+
44
+ Routing is suppressed for the same reason the view installer suppresses
45
+ it: these statements mention catalog names, and the router would answer
46
+ them with a synthetic success while nothing was created.
47
+ """
48
+ installed: list[str] = []
49
+ guard = _IN_CATALOG_HANDLER.set(True)
50
+ try:
51
+ # Verbatim: the bodies are ObjectScript, and the translation
52
+ # pipeline uppercases identifiers. It turned $SYSTEM.Encryption into
53
+ # %SYSTEM.ENCRYPTION (class names are case-sensitive) and cased the
54
+ # declared parameter differently from its uses in the body. Both
55
+ # installed cleanly and then failed on every call.
56
+ with verbatim_sql():
57
+ for function in CATALOG_FUNCTIONS:
58
+ await self._install_one(function, session_id)
59
+ installed.append(function.qualified_name)
60
+ finally:
61
+ _IN_CATALOG_HANDLER.reset(guard)
62
+
63
+ logger.info("Catalog functions installed", schema=CATALOG_SCHEMA, functions=installed)
64
+ return installed
65
+
66
+ async def _install_one(self, function: CatalogFunction, session_id: str | None) -> None:
67
+ """Execute CREATE OR REPLACE for one function, raising on failure."""
68
+ try:
69
+ result = await self._executor.execute_query(
70
+ function.create_sql(), session_id=session_id
71
+ )
72
+ except Exception as exc: # noqa: BLE001 — re-raised as an install error
73
+ raise CatalogFunctionInstallError(
74
+ f"Could not create {function.qualified_name} ({function.purpose}): {exc}"
75
+ ) from exc
76
+
77
+ if not result.get("success", False):
78
+ raise CatalogFunctionInstallError(
79
+ f"Could not create {function.qualified_name} ({function.purpose}): "
80
+ f"{result.get('error')}. The catalog views depend on it, so startup is "
81
+ "aborted rather than serving a catalog that answers with empty results."
82
+ )
83
+
84
+ async def verify(self, session_id: str | None = None) -> dict[str, bool]:
85
+ """Report which functions are present and callable."""
86
+ probes = {
87
+ "PG_OID": f"SELECT {CATALOG_SCHEMA}.PG_OID('probe')",
88
+ "PG_PUBLIC_SCHEMA": f"SELECT {CATALOG_SCHEMA}.PG_PUBLIC_SCHEMA()",
89
+ "PG_ARRAY": f"SELECT {CATALOG_SCHEMA}.PG_ARRAY('0|')",
90
+ }
91
+ status: dict[str, bool] = {}
92
+ guard = _IN_CATALOG_HANDLER.set(True)
93
+ try:
94
+ for name, probe in probes.items():
95
+ qualified = f"{CATALOG_SCHEMA}.{name}"
96
+ try:
97
+ result = await self._executor.execute_query(probe, session_id=session_id)
98
+ status[qualified] = bool(result.get("success", False))
99
+ except Exception: # noqa: BLE001 — absence is the answer, not an error
100
+ status[qualified] = False
101
+ finally:
102
+ _IN_CATALOG_HANDLER.reset(guard)
103
+ return status