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.
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/.gitignore +5 -1
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/PKG-INFO +4 -4
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/pyproject.toml +10 -2
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/__init__.py +1 -1
- iris_pgwire-1.7.1/src/iris_pgwire/catalog/_reentrancy.py +27 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/catalog_router.py +158 -15
- iris_pgwire-1.7.1/src/iris_pgwire/catalog/function_installer.py +103 -0
- iris_pgwire-1.7.1/src/iris_pgwire/catalog/functions.py +566 -0
- iris_pgwire-1.7.1/src/iris_pgwire/catalog/views/__init__.py +18 -0
- iris_pgwire-1.7.1/src/iris_pgwire/catalog/views/definitions.py +731 -0
- iris_pgwire-1.7.1/src/iris_pgwire/catalog/views/installer.py +98 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/dbapi_executor.py +153 -4
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_executor.py +335 -29
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/protocol.py +79 -21
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/schema_mapper.py +59 -1
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/server.py +46 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/array_literal.py +71 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/array_params.py +287 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/boolean_expr.py +355 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/column_types.py +197 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/normalizer.py +78 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/pg_array.py +99 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/pg_functions.py +137 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/refiner.py +25 -8
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/sqlstate.py +177 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/statement_filter.py +10 -0
- iris_pgwire-1.7.1/src/iris_pgwire/sql_translator/verbatim.py +49 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/LICENSE +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/README.md +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_column_naming.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_noop_cursor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_result_processing.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/_type_mapping.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/auth_selector.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/gssapi_auth.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/oauth_bridge.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/scram.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/auth/wallet_credentials.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/backend_selector.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/bulk_executor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/catalog_functions.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/oid_generator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_attrdef.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_class.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_index.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/catalog/pg_type.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/column_validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/config.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/config_schema.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/constitutional.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/date_horolog.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/ddl_splitter.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/json_path.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/copy_handler.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/csv_processor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/dbapi_connection_pool.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/debug_tracer.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/health_checker.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/integratedml.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_constructs.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_log_handler.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/iris_user_management.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/migrations/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/migrations/__main__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/migrations/executor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/backend_config.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/connection_pool_state.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/dbapi_connection.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/ipm_metadata.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/models/vector_query_request.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/observability.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/performance_monitor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/__main__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/documentation_validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/security_validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/quality/validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/api.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/cache.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/confidence_analyzer.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/config.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/ddl_parser.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/ddl_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/debug.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/default_values.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/error_handler.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/identifier_normalizer.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/index_validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/metrics.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/models.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/parser.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/performance_monitor.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/type_translator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/sql_translator/validator.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/testing/__init__.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/type_mapping.py +0 -0
- {iris_pgwire-1.5.9 → iris_pgwire-1.7.1}/src/iris_pgwire/vector_metrics.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: iris-pgwire
|
|
3
|
-
Version: 1.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|