iris-pgwire 1.7.3__tar.gz → 2.0.0__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 (138) hide show
  1. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/PKG-INFO +38 -7
  2. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/README.md +36 -6
  3. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/pyproject.toml +11 -1
  4. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/__init__.py +1 -1
  5. iris_pgwire-2.0.0/src/iris_pgwire/_isolation.py +413 -0
  6. iris_pgwire-2.0.0/src/iris_pgwire/auth/__init__.py +29 -0
  7. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/dbapi_executor.py +15 -1
  8. iris_pgwire-2.0.0/src/iris_pgwire/embedded_vector_fetch.py +108 -0
  9. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/iris_executor.py +104 -22
  10. iris_pgwire-2.0.0/src/iris_pgwire/password_auth.py +237 -0
  11. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/protocol.py +162 -398
  12. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/server.py +173 -7
  13. iris_pgwire-2.0.0/src/iris_pgwire/sql_translator/isolation.py +342 -0
  14. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/sqlstate.py +86 -10
  15. iris_pgwire-1.7.3/src/iris_pgwire/auth/__init__.py +0 -120
  16. iris_pgwire-1.7.3/src/iris_pgwire/auth/auth_selector.py +0 -112
  17. iris_pgwire-1.7.3/src/iris_pgwire/auth/oauth_bridge.py +0 -383
  18. iris_pgwire-1.7.3/src/iris_pgwire/auth/scram.py +0 -782
  19. iris_pgwire-1.7.3/src/iris_pgwire/auth/wallet_credentials.py +0 -313
  20. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/.gitignore +0 -0
  21. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/LICENSE +0 -0
  22. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/_column_naming.py +0 -0
  23. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/_noop_cursor.py +0 -0
  24. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/_result_processing.py +0 -0
  25. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/_type_mapping.py +0 -0
  26. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/auth/gssapi_auth.py +0 -0
  27. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/backend_selector.py +0 -0
  28. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/bulk_executor.py +0 -0
  29. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/__init__.py +0 -0
  30. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/_reentrancy.py +0 -0
  31. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/catalog_functions.py +0 -0
  32. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/catalog_router.py +0 -0
  33. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/function_installer.py +0 -0
  34. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/functions.py +0 -0
  35. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/oid_generator.py +0 -0
  36. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_attrdef.py +0 -0
  37. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
  38. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_class.py +0 -0
  39. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
  40. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_index.py +0 -0
  41. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
  42. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/pg_type.py +0 -0
  43. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/views/__init__.py +0 -0
  44. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/views/definitions.py +0 -0
  45. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/catalog/views/installer.py +0 -0
  46. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/column_validator.py +0 -0
  47. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/config.py +0 -0
  48. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/config_schema.py +0 -0
  49. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/constitutional.py +0 -0
  50. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/__init__.py +0 -0
  51. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
  52. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/date_horolog.py +0 -0
  53. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
  54. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/ddl_splitter.py +0 -0
  55. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/json_path.py +0 -0
  56. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
  57. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/copy_handler.py +0 -0
  58. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/csv_processor.py +0 -0
  59. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/dbapi_connection_pool.py +0 -0
  60. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/debug_tracer.py +0 -0
  61. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/health_checker.py +0 -0
  62. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/integratedml.py +0 -0
  63. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/iris_constructs.py +0 -0
  64. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/iris_log_handler.py +0 -0
  65. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/iris_user_management.py +0 -0
  66. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/migrations/__init__.py +0 -0
  67. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/migrations/__main__.py +0 -0
  68. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/migrations/executor.py +0 -0
  69. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/models/__init__.py +0 -0
  70. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/models/backend_config.py +0 -0
  71. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/models/connection_pool_state.py +0 -0
  72. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/models/dbapi_connection.py +0 -0
  73. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/models/ipm_metadata.py +0 -0
  74. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/models/vector_query_request.py +0 -0
  75. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/observability.py +0 -0
  76. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/performance_monitor.py +0 -0
  77. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/__init__.py +0 -0
  78. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/__main__.py +0 -0
  79. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
  80. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/documentation_validator.py +0 -0
  81. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
  82. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/security_validator.py +0 -0
  83. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/quality/validator.py +0 -0
  84. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/schema_mapper.py +0 -0
  85. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/__init__.py +0 -0
  86. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
  87. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/api.py +0 -0
  88. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/array_literal.py +0 -0
  89. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/array_params.py +0 -0
  90. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/boolean_expr.py +0 -0
  91. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
  92. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/cache.py +0 -0
  93. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/column_types.py +0 -0
  94. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/confidence_analyzer.py +0 -0
  95. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/config.py +0 -0
  96. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
  97. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
  98. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
  99. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/ddl_parser.py +0 -0
  100. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/ddl_translator.py +0 -0
  101. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/debug.py +0 -0
  102. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/default_values.py +0 -0
  103. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
  104. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
  105. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/error_handler.py +0 -0
  106. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/identifier_normalizer.py +0 -0
  107. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/index_validator.py +0 -0
  108. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
  109. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
  110. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
  111. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
  112. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
  113. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
  114. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
  115. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
  116. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/metrics.py +0 -0
  117. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/models.py +0 -0
  118. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/normalizer.py +0 -0
  119. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/parser.py +0 -0
  120. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/performance_monitor.py +0 -0
  121. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/pg_array.py +0 -0
  122. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/pg_functions.py +0 -0
  123. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
  124. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/refiner.py +0 -0
  125. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
  126. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
  127. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
  128. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/statement_filter.py +0 -0
  129. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
  130. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/translator.py +0 -0
  131. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/type_translator.py +0 -0
  132. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/validator.py +0 -0
  133. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/sql_translator/verbatim.py +0 -0
  134. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/testing/__init__.py +0 -0
  135. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
  136. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/type_mapping.py +0 -0
  137. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/vector_metrics.py +0 -0
  138. {iris_pgwire-1.7.3 → iris_pgwire-2.0.0}/src/iris_pgwire/vector_optimizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: iris-pgwire
3
- Version: 1.7.3
3
+ Version: 2.0.0
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
@@ -71,6 +71,7 @@ Provides-Extra: iris
71
71
  Provides-Extra: kerberos
72
72
  Requires-Dist: gssapi>=1.8.0; extra == 'kerberos'
73
73
  Provides-Extra: test
74
+ Requires-Dist: asyncpg>=0.29.0; extra == 'test'
74
75
  Requires-Dist: docker>=6.0.0; extra == 'test'
75
76
  Requires-Dist: iris-devtester>=0.1.0; extra == 'test'
76
77
  Requires-Dist: k5test>=0.10.3; extra == 'test'
@@ -105,8 +106,10 @@ git clone https://github.com/intersystems-community/iris-pgwire.git
105
106
  cd iris-pgwire
106
107
  docker compose up -d
107
108
 
108
- # Test it (PGWire runs on port 5432 inside the IRIS container)
109
- psql -h localhost -p 5432 -U _SYSTEM -d USER -c "SELECT 'Hello from IRIS!'"
109
+ # Test it (PGWire runs on port 5432 inside the IRIS container).
110
+ # Clients log in with an IRIS username and password; the development container accepts the
111
+ # password over a plain connection because it is published on localhost only.
112
+ PGPASSWORD=SYS psql -h localhost -p 5432 -U _SYSTEM -d USER -c "SELECT 'Hello from IRIS!'"
110
113
  ```
111
114
 
112
115
  ### Python package
@@ -117,9 +120,13 @@ pip install iris-pgwire psycopg[binary]
117
120
  export IRIS_HOST=localhost IRIS_PORT=1972 \
118
121
  IRIS_USERNAME=_SYSTEM IRIS_PASSWORD=SYS IRIS_NAMESPACE=USER
119
122
 
120
- python -m iris_pgwire.server
123
+ python -m iris_pgwire.server --allow-plaintext-password
121
124
  ```
122
125
 
126
+ Clients must log in with an IRIS username and password, and the password is sent in the clear, so
127
+ the server refuses to start unless you choose how it is protected — see
128
+ [Authentication](#authentication). `--allow-plaintext-password` is for a network you trust.
129
+
123
130
  ### First query
124
131
 
125
132
  ```python
@@ -163,7 +170,30 @@ See [Client Compatibility Guide](docs/CLIENT_RECOMMENDATIONS.md) for setup examp
163
170
 
164
171
  **SQL translation** — `RETURNING` emulation, `ON CONFLICT`, boolean literals, `pg_catalog` → `INFORMATION_SCHEMA` rewrites, JSON operators (`->` / `->>` → `JSON_EXTRACT`), parameterized queries.
165
172
 
166
- **Authentication** — SCRAM-SHA-256, OAuth 2.0 (RFC 6749), IRIS Wallet credentials.
173
+ <a id="authentication"></a>
174
+
175
+ **Authentication** — Required by default. The client sends its IRIS username and password and the
176
+ server checks them by logging in to IRIS with them, so IRIS remains the only store of users and
177
+ passwords. Because the password crosses the wire, TLS is required unless you say otherwise:
178
+
179
+ | You want | Run with |
180
+ | ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
181
+ | TLS (production) | `PGWIRE_SSL_ENABLED=true PGWIRE_SSL_CERT=… PGWIRE_SSL_KEY=…` |
182
+ | Plain connection on a trusted network, or behind a TLS-terminating proxy | `--allow-plaintext-password` (`PGWIRE_ALLOW_PLAINTEXT_PASSWORD=true`) |
183
+ | No authentication, local development only | `--trust` (`PGWIRE_AUTH_MODE=trust`) |
184
+ | Let users other than the service account log in | `--allowed-users alice,bob` (`PGWIRE_ALLOWED_USERS`) |
185
+
186
+ A server started with none of these refuses to start and says so.
187
+
188
+ What authentication does and does not do: it proves the client knows the password of an IRIS user.
189
+ Statements still run as the service account configured with `IRIS_USERNAME`, so every user who can
190
+ log in has that account's privileges. That is why **only the service account may log in unless you
191
+ list others**; a list replaces the default, so list only people you would trust with that account.
192
+ SCRAM-SHA-256, OAuth 2.0 and IRIS Wallet credentials are
193
+ **not available**: earlier versions advertised them, but the SCRAM exchange never checked the client's
194
+ proof, and `PGWIRE_ENABLE_SCRAM` was never read. Asking for SCRAM now fails at startup. Login
195
+ attempts are checked by IRIS itself, so its lockout and password policies apply — repeated failures
196
+ can lock an account.
167
197
 
168
198
  **Dual backend** — Embedded Python (irispython, lowest latency) or external DBAPI (standard TCP connection). Selectable via `IRIS_BACKEND` env var.
169
199
 
@@ -265,9 +295,10 @@ python -m iris_pgwire.quality
265
295
 
266
296
  See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for the full list. Key items:
267
297
 
298
+ - **No MVCC or snapshot isolation.** `REPEATABLE READ` and `SERIALIZABLE` are rejected rather than silently downgraded; IRIS `READ COMMITTED` is narrower than PostgreSQL's, and dirty reads are reachable through aggregates, `GROUP BY` and `DISTINCT`. Lock conflicts arrive as retryable `40001`. Version-guarded (optimistic-locking) updates can report false conflicts — read the isolation section before porting an ORM that uses them.
268
299
  - IRIS Community Edition: 5-user connection limit — use `PGWIRE_POOL_SIZE=1` for dev
269
- - No native SSL termination — use nginx/HAProxy in front for TLS
270
- - Kerberos/GSSAPI auth wiring deferred (OAuth 2.0 is the recommended enterprise auth)
300
+ - TLS is native (`PGWIRE_SSL_ENABLED`); behind a TLS-terminating proxy, use `--allow-plaintext-password`
301
+ - Kerberos/GSSAPI auth wiring deferred; SCRAM-SHA-256, OAuth 2.0 and Wallet credentials are not available (see [Authentication](#authentication))
271
302
  - `INFORMATION_SCHEMA` only, no `pg_catalog` tables (translated automatically)
272
303
 
273
304
  ---
@@ -19,8 +19,10 @@ git clone https://github.com/intersystems-community/iris-pgwire.git
19
19
  cd iris-pgwire
20
20
  docker compose up -d
21
21
 
22
- # Test it (PGWire runs on port 5432 inside the IRIS container)
23
- psql -h localhost -p 5432 -U _SYSTEM -d USER -c "SELECT 'Hello from IRIS!'"
22
+ # Test it (PGWire runs on port 5432 inside the IRIS container).
23
+ # Clients log in with an IRIS username and password; the development container accepts the
24
+ # password over a plain connection because it is published on localhost only.
25
+ PGPASSWORD=SYS psql -h localhost -p 5432 -U _SYSTEM -d USER -c "SELECT 'Hello from IRIS!'"
24
26
  ```
25
27
 
26
28
  ### Python package
@@ -31,9 +33,13 @@ pip install iris-pgwire psycopg[binary]
31
33
  export IRIS_HOST=localhost IRIS_PORT=1972 \
32
34
  IRIS_USERNAME=_SYSTEM IRIS_PASSWORD=SYS IRIS_NAMESPACE=USER
33
35
 
34
- python -m iris_pgwire.server
36
+ python -m iris_pgwire.server --allow-plaintext-password
35
37
  ```
36
38
 
39
+ Clients must log in with an IRIS username and password, and the password is sent in the clear, so
40
+ the server refuses to start unless you choose how it is protected — see
41
+ [Authentication](#authentication). `--allow-plaintext-password` is for a network you trust.
42
+
37
43
  ### First query
38
44
 
39
45
  ```python
@@ -77,7 +83,30 @@ See [Client Compatibility Guide](docs/CLIENT_RECOMMENDATIONS.md) for setup examp
77
83
 
78
84
  **SQL translation** — `RETURNING` emulation, `ON CONFLICT`, boolean literals, `pg_catalog` → `INFORMATION_SCHEMA` rewrites, JSON operators (`->` / `->>` → `JSON_EXTRACT`), parameterized queries.
79
85
 
80
- **Authentication** — SCRAM-SHA-256, OAuth 2.0 (RFC 6749), IRIS Wallet credentials.
86
+ <a id="authentication"></a>
87
+
88
+ **Authentication** — Required by default. The client sends its IRIS username and password and the
89
+ server checks them by logging in to IRIS with them, so IRIS remains the only store of users and
90
+ passwords. Because the password crosses the wire, TLS is required unless you say otherwise:
91
+
92
+ | You want | Run with |
93
+ | ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
94
+ | TLS (production) | `PGWIRE_SSL_ENABLED=true PGWIRE_SSL_CERT=… PGWIRE_SSL_KEY=…` |
95
+ | Plain connection on a trusted network, or behind a TLS-terminating proxy | `--allow-plaintext-password` (`PGWIRE_ALLOW_PLAINTEXT_PASSWORD=true`) |
96
+ | No authentication, local development only | `--trust` (`PGWIRE_AUTH_MODE=trust`) |
97
+ | Let users other than the service account log in | `--allowed-users alice,bob` (`PGWIRE_ALLOWED_USERS`) |
98
+
99
+ A server started with none of these refuses to start and says so.
100
+
101
+ What authentication does and does not do: it proves the client knows the password of an IRIS user.
102
+ Statements still run as the service account configured with `IRIS_USERNAME`, so every user who can
103
+ log in has that account's privileges. That is why **only the service account may log in unless you
104
+ list others**; a list replaces the default, so list only people you would trust with that account.
105
+ SCRAM-SHA-256, OAuth 2.0 and IRIS Wallet credentials are
106
+ **not available**: earlier versions advertised them, but the SCRAM exchange never checked the client's
107
+ proof, and `PGWIRE_ENABLE_SCRAM` was never read. Asking for SCRAM now fails at startup. Login
108
+ attempts are checked by IRIS itself, so its lockout and password policies apply — repeated failures
109
+ can lock an account.
81
110
 
82
111
  **Dual backend** — Embedded Python (irispython, lowest latency) or external DBAPI (standard TCP connection). Selectable via `IRIS_BACKEND` env var.
83
112
 
@@ -179,9 +208,10 @@ python -m iris_pgwire.quality
179
208
 
180
209
  See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for the full list. Key items:
181
210
 
211
+ - **No MVCC or snapshot isolation.** `REPEATABLE READ` and `SERIALIZABLE` are rejected rather than silently downgraded; IRIS `READ COMMITTED` is narrower than PostgreSQL's, and dirty reads are reachable through aggregates, `GROUP BY` and `DISTINCT`. Lock conflicts arrive as retryable `40001`. Version-guarded (optimistic-locking) updates can report false conflicts — read the isolation section before porting an ORM that uses them.
182
212
  - IRIS Community Edition: 5-user connection limit — use `PGWIRE_POOL_SIZE=1` for dev
183
- - No native SSL termination — use nginx/HAProxy in front for TLS
184
- - Kerberos/GSSAPI auth wiring deferred (OAuth 2.0 is the recommended enterprise auth)
213
+ - TLS is native (`PGWIRE_SSL_ENABLED`); behind a TLS-terminating proxy, use `--allow-plaintext-password`
214
+ - Kerberos/GSSAPI auth wiring deferred; SCRAM-SHA-256, OAuth 2.0 and Wallet credentials are not available (see [Authentication](#authentication))
185
215
  - `INFORMATION_SCHEMA` only, no `pg_catalog` tables (translated automatically)
186
216
 
187
217
  ---
@@ -81,6 +81,11 @@ test = [
81
81
  # [binary] includes C extension + libpq, no system dependencies needed
82
82
  "psycopg[binary]>=3.1.0",
83
83
  "docker>=6.0.0",
84
+ # asyncpg is a client we claim compatibility with, and
85
+ # tests/client_compatibility/python imports it at module scope. Undeclared,
86
+ # those modules fail to *collect*, which aborts `pytest tests/` before it
87
+ # runs anything rather than skipping two files.
88
+ "asyncpg>=0.29.0",
84
89
  "iris-devtester>=0.1.0",
85
90
  "pytest-benchmark>=4.0.0",
86
91
  # Runtime dependencies needed when importing iris_pgwire modules in tests
@@ -117,7 +122,7 @@ kerberos = [
117
122
  ]
118
123
 
119
124
  [project.scripts]
120
- iris-pgwire = "iris_pgwire.server:main"
125
+ iris-pgwire = "iris_pgwire.server:cli"
121
126
 
122
127
  [project.urls]
123
128
  Homepage = "https://github.com/intersystems-community/iris-pgwire"
@@ -284,6 +289,11 @@ markers = [
284
289
  "document_db: Tests for document database features",
285
290
  "error_handling: Tests for error handling scenarios",
286
291
  "mixed_sql: Tests for mixed SQL query processing",
292
+ # A pinned limitation asserts behaviour that is *wrong* by PostgreSQL's
293
+ # standards and cannot be fixed at the wire layer. It is pinned, not endorsed:
294
+ # a failure means the limitation changed and KNOWN_LIMITATIONS.md is now
295
+ # lying. Run them together with `pytest -m pinned_limitation`.
296
+ "pinned_limitation: Pins a documented divergence from PostgreSQL; a failure means the docs are stale",
287
297
  ]
288
298
 
289
299
  # Flaky test detection configuration
@@ -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.7.3"
9
+ __version__ = "2.0.0"
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,413 @@
1
+ """Transaction isolation level handling, shared by both backends.
2
+
3
+ This lives in a mixin rather than in either executor because the two backends
4
+ have different code paths to IRIS and a client cannot tell which one it is
5
+ talking to. Isolation handled in only one of them is isolation handled in none:
6
+ the embedded backend serves `iris session`-style execution while DBAPI serves
7
+ `intersystems-irispython`, and the pgwire server picks between them by config.
8
+
9
+ The parsing and the measured IRIS support table are in
10
+ sql_translator/isolation.py; this module is only the wiring and the per-session
11
+ bookkeeping.
12
+
13
+ Two settings, not one. `transaction_isolation` is the level the current
14
+ transaction runs at and `default_transaction_isolation` is what the next one
15
+ starts at, and PostgreSQL's spellings write one or the other — see the measured
16
+ table in tests/unit/test_isolation_scopes.py. IRIS has a single level per
17
+ session, so the two slots are kept here and IRIS is told whichever one is in
18
+ force; `_resync_iris` is what keeps that promise when the level changes because
19
+ a transaction ended rather than because a client asked.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ import structlog
28
+
29
+ from .sql_translator.isolation import (
30
+ DEFAULT_ISOLATION_LEVEL,
31
+ MID_TRANSACTION_AFTER_QUERY,
32
+ MID_TRANSACTION_CONDITION,
33
+ MID_TRANSACTION_IN_SUBTRANSACTION,
34
+ MID_TRANSACTION_SQLSTATE,
35
+ SESSION_SCOPE,
36
+ counts_as_query,
37
+ parse_begin_isolation,
38
+ parse_isolation_show,
39
+ parse_savepoint_change,
40
+ parse_set_isolation,
41
+ parse_transaction_boundary,
42
+ )
43
+
44
+ logger = structlog.get_logger("iris_pgwire.isolation")
45
+
46
+
47
+ @dataclass
48
+ class _SessionIsolation:
49
+ """The per-session state that is not the session default.
50
+
51
+ The session default lives in the host class's ``session_isolation`` dict,
52
+ which predates this and is read by name elsewhere. Everything else is here.
53
+ """
54
+
55
+ # The transaction-scoped level, in force until the transaction ends.
56
+ override: str | None = None
57
+ # The level IRIS was last told, so a divergence can be noticed and repaired.
58
+ iris_level: str | None = None
59
+ # Whether a transaction is open, which decides whether a session-default
60
+ # change is undoable and whether a resync would land mid-transaction.
61
+ in_transaction: bool = False
62
+ # The session default as it was when this transaction opened, kept only once
63
+ # and only if the transaction changed it — PostgreSQL rolls `SET` back too.
64
+ default_before_transaction: str | None = None
65
+ default_changed_in_transaction: bool = False
66
+ # Whether this transaction has read anything, after which its level can no
67
+ # longer be changed, and how deep it is in savepoints, which is the same rule
68
+ # for a different reason.
69
+ transaction_did_query: bool = False
70
+ savepoint_depth: int = 0
71
+
72
+
73
+ class IsolationMixin:
74
+ """`SET`/`SHOW TRANSACTION ISOLATION LEVEL` against real IRIS state.
75
+
76
+ The host class must provide an awaitable
77
+ ``execute_query(sql, params=None, session_id=None, **kwargs)`` and must
78
+ create ``self.session_isolation`` in its ``__init__``.
79
+ """
80
+
81
+ session_isolation: dict[str, str]
82
+
83
+ # ------------------------------------------------------------------
84
+ # Per-session state
85
+ # ------------------------------------------------------------------
86
+
87
+ @property
88
+ def _isolation_sessions(self) -> dict[str, _SessionIsolation]:
89
+ """Created on first use, so a host class only has to know about one dict."""
90
+ store = getattr(self, "_isolation_sessions_store", None)
91
+ if store is None:
92
+ store = {}
93
+ self._isolation_sessions_store: dict[str, _SessionIsolation] = store
94
+ return store
95
+
96
+ def _isolation_state(self, session_id: str) -> _SessionIsolation:
97
+ return self._isolation_sessions.setdefault(session_id, _SessionIsolation())
98
+
99
+ def get_isolation_level(self, session_id: str | None) -> str:
100
+ """`transaction_isolation`: the level this session is running at now.
101
+
102
+ The transaction's own level if it has one, else the session default, else
103
+ READ UNCOMMITTED — which is what IRIS runs at until told otherwise, not
104
+ PostgreSQL's READ COMMITTED default.
105
+ """
106
+ if session_id is None:
107
+ return DEFAULT_ISOLATION_LEVEL
108
+ state = self._isolation_sessions.get(session_id)
109
+ if state is not None and state.override is not None:
110
+ return state.override
111
+ return self.get_default_isolation_level(session_id)
112
+
113
+ def get_default_isolation_level(self, session_id: str | None) -> str:
114
+ """`default_transaction_isolation`: what the next transaction starts at.
115
+
116
+ Deliberately blind to the current transaction's override. A pool asking
117
+ this is asking what it handed out, and answering with a level that ends
118
+ at the next COMMIT would be the wrong answer to that question.
119
+ """
120
+ if session_id is None:
121
+ return DEFAULT_ISOLATION_LEVEL
122
+ return self.session_isolation.get(session_id, DEFAULT_ISOLATION_LEVEL)
123
+
124
+ def forget_isolation_level(self, session_id: str | None) -> None:
125
+ """Drop a closed session's state so the dicts cannot grow without bound.
126
+
127
+ Both slots, because session ids are reused: a leftover override would be
128
+ the previous client's, and would be resynced onto IRIS as if asked for.
129
+ """
130
+ if session_id is not None:
131
+ self.session_isolation.pop(session_id, None)
132
+ self._isolation_sessions.pop(session_id, None)
133
+
134
+ # ------------------------------------------------------------------
135
+ # Statement handling
136
+ # ------------------------------------------------------------------
137
+
138
+ def _isolation_result(self, guc: str, level: str) -> dict[str, Any]:
139
+ """A one-row, one-column SHOW reply carrying `level`."""
140
+ return {
141
+ "success": True,
142
+ "rows": [[level]],
143
+ "columns": [
144
+ {
145
+ "name": guc,
146
+ "type_oid": 25, # TEXT
147
+ "type_size": -1,
148
+ "type_modifier": -1,
149
+ "format_code": 0,
150
+ }
151
+ ],
152
+ "row_count": 1,
153
+ "command_tag": "SHOW",
154
+ "execution_time_ms": 0.0,
155
+ }
156
+
157
+ async def _handle_isolation_statement(
158
+ self, sql: str, session_id: str | None = None, **kwargs
159
+ ) -> dict[str, Any] | None:
160
+ """Handle isolation-level SET and SHOW, or return None to pass `sql` on.
161
+
162
+ Called before the SQL interceptor in both executors, because the
163
+ interceptor's generic SHOW handler answers anything it does not
164
+ recognise with the literal string "unknown". Being on the path of every
165
+ statement is also what lets the transaction boundary be seen, which is
166
+ where a transaction-scoped level ends.
167
+
168
+ A supported SET is executed against IRIS for real and the session's level
169
+ is recorded only once IRIS has accepted it, so `SHOW` can never report a
170
+ level that was not applied. An unsupported one is refused — see
171
+ sql_translator/isolation.py for why it is not downgraded instead.
172
+ """
173
+ guc = parse_isolation_show(sql)
174
+ if guc is not None:
175
+ level = (
176
+ self.get_default_isolation_level(session_id)
177
+ if guc == "default_transaction_isolation"
178
+ else self.get_isolation_level(session_id)
179
+ )
180
+ return self._isolation_result(guc, level)
181
+
182
+ request = parse_set_isolation(sql)
183
+ if request is not None:
184
+ if not request.supported:
185
+ # The level is checked before the timing, which is the order
186
+ # PostgreSQL measurably uses: an unusable level is refused on its
187
+ # own terms whether or not it arrived too late.
188
+ return self._refuse(request, session_id)
189
+ too_late = self._too_late_to_change(request, session_id)
190
+ if too_late is not None:
191
+ return too_late
192
+ return await self._apply(request, session_id, **kwargs)
193
+
194
+ # `BEGIN ISOLATION LEVEL x` is the same request in the spelling psycopg3
195
+ # sends, and it must be refused and recorded the same way — otherwise a
196
+ # client routes around the refusal by naming the level on BEGIN instead.
197
+ request = parse_begin_isolation(sql)
198
+ if request is not None:
199
+ if not request.supported:
200
+ # Refused before the transaction starts, so the session is left
201
+ # where it was rather than inside a transaction it did not ask for.
202
+ return self._refuse(request, session_id)
203
+
204
+ self._note_transaction_boundary("start", session_id)
205
+ # Still one statement, in IRIS's spelling of it — `BEGIN` is not IRIS
206
+ # SQL and this interception happens ahead of the translator that would
207
+ # have rewritten it. The request carries the tag for the verb the
208
+ # client actually used, so the rewrite stays invisible in the reply.
209
+ return await self._apply(request, session_id, **kwargs)
210
+
211
+ boundary = parse_transaction_boundary(sql)
212
+ if boundary == "start":
213
+ # Before the transaction opens is the last moment IRIS's level can be
214
+ # corrected, since changing it inside a transaction is what PostgreSQL
215
+ # refuses. A transaction that inherited the wrong level would run at it
216
+ # from its first statement to its last.
217
+ failed = await self._resync_iris(session_id, **kwargs)
218
+ if failed is not None:
219
+ return failed
220
+ self._note_transaction_boundary(boundary, session_id)
221
+ # Not our statement: the BEGIN itself still has to reach IRIS.
222
+ return None
223
+
224
+ if boundary is not None:
225
+ self._note_transaction_boundary(boundary, session_id)
226
+ return None
227
+
228
+ self._note_ordinary_statement(sql, session_id)
229
+ return await self._resync_iris(session_id, **kwargs)
230
+
231
+ def _refuse(self, request, session_id: str | None) -> dict[str, Any]:
232
+ """The error reply for a level IRIS cannot honour."""
233
+ logger.info(
234
+ "Refusing unsupported transaction isolation level",
235
+ requested_level=request.level,
236
+ session_id=session_id,
237
+ )
238
+ return {
239
+ "success": False,
240
+ "error": request.message,
241
+ "sqlstate": request.sqlstate,
242
+ "rows": [],
243
+ "columns": [],
244
+ "row_count": 0,
245
+ }
246
+
247
+ async def _apply(self, request, session_id: str | None, **kwargs) -> dict[str, Any]:
248
+ """Send the request's IRIS statement and record the level only if IRIS took it."""
249
+ # `_skip_isolation` stops this from recursing on the statement we send.
250
+ result = await self.execute_query( # type: ignore[attr-defined]
251
+ request.iris_sql, session_id=session_id, _skip_isolation=True, **kwargs
252
+ )
253
+ if result.get("success") and session_id is not None:
254
+ self._record_applied(request, session_id)
255
+ if request.command_tag is not None:
256
+ result["command_tag"] = request.command_tag
257
+ return result
258
+
259
+ def _record_applied(self, request, session_id: str) -> None:
260
+ """Write the accepted level into the slot its spelling names."""
261
+ state = self._isolation_state(session_id)
262
+ state.iris_level = request.level
263
+
264
+ if request.scope == SESSION_SCOPE:
265
+ if state.in_transaction and not state.default_changed_in_transaction:
266
+ # Snapshot once: a ROLLBACK restores the value from before the
267
+ # transaction, not from before the most recent SET inside it.
268
+ state.default_before_transaction = self.session_isolation.get(session_id)
269
+ state.default_changed_in_transaction = True
270
+ self.session_isolation[session_id] = request.level
271
+ # The default is what the current transaction runs at too, unless the
272
+ # transaction named its own level — so an override still wins.
273
+ else:
274
+ state.override = request.level
275
+
276
+ logger.info(
277
+ "Transaction isolation level applied",
278
+ level=request.level,
279
+ scope=request.scope,
280
+ session_id=session_id,
281
+ )
282
+
283
+ def _too_late_to_change(self, request, session_id: str | None) -> dict[str, Any] | None:
284
+ """The 25001 reply if this transaction is past changing its level, else None.
285
+
286
+ Only the transaction-scoped spellings can be too late; the session-scoped
287
+ ones describe the next transaction. Re-asserting the level already in
288
+ force is always allowed, measured on PostgreSQL 16 and relied on by every
289
+ client that restates its isolation level defensively.
290
+ """
291
+ if session_id is None or request.scope == SESSION_SCOPE:
292
+ return None
293
+ state = self._isolation_sessions.get(session_id)
294
+ if state is None or not state.in_transaction:
295
+ return None
296
+ if request.level == self.get_isolation_level(session_id):
297
+ return None
298
+
299
+ if state.savepoint_depth > 0:
300
+ message = MID_TRANSACTION_IN_SUBTRANSACTION
301
+ elif state.transaction_did_query:
302
+ message = MID_TRANSACTION_AFTER_QUERY
303
+ else:
304
+ return None
305
+
306
+ logger.info(
307
+ "Refusing a mid-transaction isolation level change",
308
+ requested_level=request.level,
309
+ current_level=self.get_isolation_level(session_id),
310
+ session_id=session_id,
311
+ )
312
+ # Not recorded and not sent: the transaction keeps the level it has, and
313
+ # `SHOW` keeps reporting it.
314
+ return {
315
+ "success": False,
316
+ "error": message,
317
+ "sqlstate": MID_TRANSACTION_SQLSTATE,
318
+ "condition": MID_TRANSACTION_CONDITION,
319
+ "rows": [],
320
+ "columns": [],
321
+ "row_count": 0,
322
+ }
323
+
324
+ def _note_ordinary_statement(self, sql: str, session_id: str | None) -> None:
325
+ """Track what an open transaction has done, which decides the 25001 above."""
326
+ if session_id is None:
327
+ return
328
+ savepoint = parse_savepoint_change(sql)
329
+ if savepoint is not None:
330
+ state = self._isolation_state(session_id)
331
+ if not state.in_transaction:
332
+ # A stray savepoint statement outside a transaction; PostgreSQL
333
+ # has nothing to nest and neither have we.
334
+ return
335
+ if savepoint == "enter":
336
+ state.savepoint_depth += 1
337
+ else:
338
+ state.savepoint_depth = max(0, state.savepoint_depth - 1)
339
+ return
340
+
341
+ if not counts_as_query(sql):
342
+ return
343
+ state = self._isolation_sessions.get(session_id)
344
+ if state is not None and state.in_transaction:
345
+ state.transaction_did_query = True
346
+
347
+ def _note_transaction_boundary(self, boundary: str, session_id: str | None) -> None:
348
+ """Open or close the window a transaction-scoped level lives in."""
349
+ if session_id is None:
350
+ return
351
+ state = self._isolation_state(session_id)
352
+
353
+ if boundary == "start":
354
+ state.in_transaction = True
355
+ return
356
+
357
+ if boundary == "rollback" and state.default_changed_in_transaction:
358
+ # Measured on PostgreSQL 16: a session default set inside a
359
+ # transaction that rolls back never happened. Savepoints are not
360
+ # modelled — a partial rollback keeps the whole change.
361
+ previous = state.default_before_transaction
362
+ if previous is None:
363
+ self.session_isolation.pop(session_id, None)
364
+ else:
365
+ self.session_isolation[session_id] = previous
366
+
367
+ state.override = None
368
+ state.in_transaction = False
369
+ state.default_before_transaction = None
370
+ state.default_changed_in_transaction = False
371
+ state.transaction_did_query = False
372
+ state.savepoint_depth = 0
373
+
374
+ async def _resync_iris(self, session_id: str | None, **kwargs) -> dict[str, Any] | None:
375
+ """Put IRIS back on the level we report, or return None to carry on.
376
+
377
+ The slots are bookkeeping and IRIS has one level per session, so ending a
378
+ transaction that had its own level changes what we report without any
379
+ statement having said so. Left alone, `SHOW` would go back to naming a
380
+ level the session is not running at, which is the whole defect this
381
+ module exists to fix — arriving by a different route.
382
+
383
+ An error here is returned to the client rather than swallowed: carrying on
384
+ would mean running the statement at a level we are about to misreport.
385
+ """
386
+ if session_id is None:
387
+ return None
388
+ state = self._isolation_sessions.get(session_id)
389
+ if state is None or state.iris_level is None or state.in_transaction:
390
+ return None
391
+
392
+ wanted = self.get_isolation_level(session_id)
393
+ if wanted == state.iris_level:
394
+ return None
395
+
396
+ request = parse_set_isolation(f"SET TRANSACTION ISOLATION LEVEL {wanted}")
397
+ if request is None or not request.supported: # pragma: no cover - defensive
398
+ return None
399
+
400
+ logger.info(
401
+ "Resyncing IRIS to the level now in force",
402
+ was=state.iris_level,
403
+ now=wanted,
404
+ session_id=session_id,
405
+ )
406
+ result = await self.execute_query( # type: ignore[attr-defined]
407
+ request.iris_sql, session_id=session_id, _skip_isolation=True, **kwargs
408
+ )
409
+ if not result.get("success"):
410
+ return result
411
+
412
+ state.iris_level = wanted
413
+ return None
@@ -0,0 +1,29 @@
1
+ """Authentication components that are not wired into the server.
2
+
3
+ What the server actually does is in ``iris_pgwire.password_auth``: the client sends its IRIS
4
+ password and the server checks it by logging in to IRIS.
5
+
6
+ This package once also held SCRAM-SHA-256, OAuth 2.0 and IRIS Wallet code (feature 024). It was
7
+ removed on 2026-10-24. None of it was ever reachable in a way that verified anything: the SCRAM
8
+ exchange accepted any client proof, handed the next stage the literal password
9
+ ``"placeholder_password"`` and answered with a constant server signature. It is in git history,
10
+ and ``docs/adr/0003-password-auth-verified-by-iris-login.md`` records why it was not kept.
11
+
12
+ What remains is the Kerberos/GSSAPI authenticator, which is also not wired in.
13
+ """
14
+
15
+ from .gssapi_auth import (
16
+ GSSAPIAuthenticator,
17
+ KerberosAuthenticationError,
18
+ KerberosConfig,
19
+ KerberosPrincipal,
20
+ KerberosTimeoutError,
21
+ )
22
+
23
+ __all__ = [
24
+ "GSSAPIAuthenticator",
25
+ "KerberosAuthenticationError",
26
+ "KerberosConfig",
27
+ "KerberosPrincipal",
28
+ "KerberosTimeoutError",
29
+ ]