iris-pgwire 1.5.0__tar.gz → 1.5.2__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 (119) hide show
  1. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/PKG-INFO +1 -1
  2. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/__init__.py +1 -1
  3. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/backend_selector.py +1 -0
  4. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/bulk_executor.py +28 -11
  5. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/dbapi_executor.py +23 -2
  6. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/iris_executor.py +42 -4
  7. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/models/backend_config.py +13 -0
  8. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/server.py +4 -0
  9. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/.gitignore +0 -0
  10. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/LICENSE +0 -0
  11. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/README.md +0 -0
  12. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/pyproject.toml +0 -0
  13. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/_column_naming.py +0 -0
  14. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/_noop_cursor.py +0 -0
  15. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/_result_processing.py +0 -0
  16. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/_type_mapping.py +0 -0
  17. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/auth/__init__.py +0 -0
  18. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/auth/auth_selector.py +0 -0
  19. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/auth/gssapi_auth.py +0 -0
  20. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/auth/oauth_bridge.py +0 -0
  21. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/auth/scram.py +0 -0
  22. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/auth/wallet_credentials.py +0 -0
  23. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/__init__.py +0 -0
  24. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/catalog_functions.py +0 -0
  25. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/catalog_router.py +0 -0
  26. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/oid_generator.py +0 -0
  27. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_attrdef.py +0 -0
  28. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
  29. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_class.py +0 -0
  30. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
  31. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_index.py +0 -0
  32. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
  33. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/catalog/pg_type.py +0 -0
  34. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/column_validator.py +0 -0
  35. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/config.py +0 -0
  36. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/config_schema.py +0 -0
  37. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/constitutional.py +0 -0
  38. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/__init__.py +0 -0
  39. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
  40. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/date_horolog.py +0 -0
  41. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
  42. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/ddl_splitter.py +0 -0
  43. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/json_path.py +0 -0
  44. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
  45. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/copy_handler.py +0 -0
  46. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/csv_processor.py +0 -0
  47. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/dbapi_connection_pool.py +0 -0
  48. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/debug_tracer.py +0 -0
  49. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/health_checker.py +0 -0
  50. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/integratedml.py +0 -0
  51. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/iris_constructs.py +0 -0
  52. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/iris_log_handler.py +0 -0
  53. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/iris_user_management.py +0 -0
  54. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/migrations/__init__.py +0 -0
  55. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/migrations/__main__.py +0 -0
  56. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/migrations/executor.py +0 -0
  57. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/models/__init__.py +0 -0
  58. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/models/connection_pool_state.py +0 -0
  59. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/models/dbapi_connection.py +0 -0
  60. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/models/ipm_metadata.py +0 -0
  61. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/models/vector_query_request.py +0 -0
  62. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/observability.py +0 -0
  63. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/performance_monitor.py +0 -0
  64. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/protocol.py +0 -0
  65. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/__init__.py +0 -0
  66. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/__main__.py +0 -0
  67. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
  68. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/documentation_validator.py +0 -0
  69. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
  70. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/security_validator.py +0 -0
  71. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/quality/validator.py +0 -0
  72. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/schema_mapper.py +0 -0
  73. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/__init__.py +0 -0
  74. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
  75. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/api.py +0 -0
  76. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
  77. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/cache.py +0 -0
  78. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/confidence_analyzer.py +0 -0
  79. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/config.py +0 -0
  80. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
  81. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
  82. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
  83. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/ddl_parser.py +0 -0
  84. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/ddl_translator.py +0 -0
  85. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/debug.py +0 -0
  86. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/default_values.py +0 -0
  87. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
  88. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
  89. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/error_handler.py +0 -0
  90. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/identifier_normalizer.py +0 -0
  91. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/index_validator.py +0 -0
  92. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
  93. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
  94. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
  95. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
  96. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
  97. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
  98. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
  99. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
  100. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/metrics.py +0 -0
  101. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/models.py +0 -0
  102. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/normalizer.py +0 -0
  103. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/parser.py +0 -0
  104. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/performance_monitor.py +0 -0
  105. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
  106. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/refiner.py +0 -0
  107. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
  108. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
  109. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
  110. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/statement_filter.py +0 -0
  111. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
  112. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/translator.py +0 -0
  113. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/type_translator.py +0 -0
  114. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/sql_translator/validator.py +0 -0
  115. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/testing/__init__.py +0 -0
  116. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
  117. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/type_mapping.py +0 -0
  118. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/vector_metrics.py +0 -0
  119. {iris_pgwire-1.5.0 → iris_pgwire-1.5.2}/src/iris_pgwire/vector_optimizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-pgwire
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -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.0"
9
+ __version__ = "1.5.2"
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
@@ -265,6 +265,7 @@ class BackendSelector:
265
265
  connection_pool_size=config.pool_size,
266
266
  connection_pool_timeout=float(config.pool_timeout),
267
267
  strict_single_connection=config.strict_single_connection,
268
+ query_timeout=config.query_timeout,
268
269
  )
269
270
  # backend_type is now set in IRISExecutor.__init__
270
271
  logger.info(
@@ -71,10 +71,23 @@ class BulkExecutor:
71
71
  total_rows = 0
72
72
  batch = []
73
73
  actual_column_names = column_names
74
+ # Lazily resolved from schema when CSV has no header and no column list
75
+ schema_columns: list[str] | None = None
74
76
 
75
77
  async for row_dict in rows:
76
- # Determine column names from first row if not specified
77
- if actual_column_names is None:
78
+ # When column_names is None and the CSV has no header, the CSV processor
79
+ # generates placeholder keys (column_0, column_1, …). Detect this on
80
+ # the first row, fetch real column names from IRIS schema, and re-map.
81
+ first_key = next(iter(row_dict), None)
82
+ if first_key is not None and first_key.startswith("column_"):
83
+ if schema_columns is None:
84
+ schema_columns = await self.get_table_columns(table_name)
85
+ logger.debug(f"Columns fetched from schema for {table_name}: {schema_columns}")
86
+ if schema_columns:
87
+ actual_column_names = schema_columns
88
+ values = list(row_dict.values())
89
+ row_dict = dict(zip(schema_columns, values))
90
+ elif actual_column_names is None:
78
91
  actual_column_names = list(row_dict.keys())
79
92
  logger.debug(f"Columns inferred from data: {actual_column_names}")
80
93
 
@@ -321,25 +334,29 @@ class BulkExecutor:
321
334
  """
322
335
  Get column names for a table using INFORMATION_SCHEMA.
323
336
 
337
+ Bypasses the catalog router (which intercepts INFORMATION_SCHEMA queries
338
+ for pg_catalog emulation) by calling _execute_external_async directly.
339
+
324
340
  Args:
325
341
  table_name: Table name
326
342
 
327
343
  Returns:
328
- List of column names
344
+ List of column names in ordinal order
329
345
 
330
346
  Raises:
331
347
  Exception: IRIS query error
332
348
  """
333
- query = f"""
334
- SELECT column_name
335
- FROM INFORMATION_SCHEMA.COLUMNS
336
- WHERE LOWER(table_name) = LOWER('{table_name}')
337
- ORDER BY ordinal_position
338
- """
349
+ query = (
350
+ "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS "
351
+ f"WHERE LOWER(TABLE_NAME) = LOWER('{table_name}') "
352
+ "ORDER BY ORDINAL_POSITION"
353
+ )
339
354
 
340
- result = await self.iris_executor.execute_query(query, [])
355
+ # Bypass iris_executor.execute_query (which routes through the catalog
356
+ # router and returns empty rows for INFORMATION_SCHEMA queries) by
357
+ # calling the external execution path directly.
358
+ result = await self.iris_executor._execute_external_async(query, [])
341
359
 
342
- # Extract column names from result
343
360
  columns = []
344
361
  if result and "rows" in result:
345
362
  columns = [row[0] for row in result["rows"]]
@@ -202,7 +202,25 @@ class DBAPIExecutor:
202
202
  conn_wrapper.connection, plan, converted_params, session_id, original_sql
203
203
  )
204
204
 
205
- rows, columns, row_count = await asyncio.to_thread(execute_in_thread)
205
+ query_timeout = getattr(self.config, "query_timeout", 30.0)
206
+ try:
207
+ rows, columns, row_count = await asyncio.wait_for(
208
+ asyncio.to_thread(execute_in_thread), timeout=query_timeout
209
+ )
210
+ except TimeoutError:
211
+ logger.error(
212
+ "Query timed out — evicting connection from pool",
213
+ sql_preview=sql[:120],
214
+ timeout_seconds=query_timeout,
215
+ session_id=session_id,
216
+ )
217
+ # Mark connection unhealthy so it gets evicted, not recycled
218
+ if conn_wrapper:
219
+ conn_wrapper.is_healthy = False
220
+ raise RuntimeError(
221
+ f"Query execution timed out after {query_timeout}s. "
222
+ "The IRIS connection is being evicted to prevent pool exhaustion."
223
+ )
206
224
 
207
225
  tx_sql_upper = original_sql.strip().upper()
208
226
  self._update_transaction_state(session_id, tx_sql_upper)
@@ -1044,7 +1062,10 @@ class DBAPIExecutor:
1044
1062
  for i, col in enumerate(columns):
1045
1063
  if col in ("id", "uuid", "_id"):
1046
1064
  if params and len(params) > i:
1047
- return col, params[i]
1065
+ # Return UPPERCASE so the WHERE clause matches IRIS's stored column name.
1066
+ # IRIS stores unquoted column names as uppercase; using "id" (lowercase)
1067
+ # in a quoted identifier fails silently and returns 0 rows.
1068
+ return col.upper(), params[i]
1048
1069
  return None, None
1049
1070
 
1050
1071
  return None, None
@@ -100,6 +100,7 @@ class IRISExecutor:
100
100
  enable_query_cache: bool = True,
101
101
  query_cache_size: int = 1000,
102
102
  strict_single_connection: bool = False,
103
+ query_timeout: float = 30.0,
103
104
  ):
104
105
  self.iris_config = iris_config
105
106
  self.server = server # Reference to server for P4 cancellation
@@ -108,6 +109,7 @@ class IRISExecutor:
108
109
  self.enable_query_cache = enable_query_cache
109
110
  self.query_cache_size = query_cache_size
110
111
  self.strict_single_connection = strict_single_connection
112
+ self.query_timeout = query_timeout
111
113
 
112
114
  self.connection = None
113
115
  self.session_connections = {}
@@ -1763,7 +1765,10 @@ class IRISExecutor:
1763
1765
  for i, col in enumerate(columns):
1764
1766
  if col in id_col_names:
1765
1767
  id_col_idx = i
1766
- id_col_name = col
1768
+ # Return UPPERCASE so the WHERE clause matches IRIS's stored column name.
1769
+ # IRIS stores unquoted column names as uppercase; using "id" (lowercase)
1770
+ # in a quoted identifier fails silently and returns 0 rows.
1771
+ id_col_name = col.upper()
1767
1772
  break
1768
1773
 
1769
1774
  if id_col_idx is None:
@@ -2884,6 +2889,12 @@ class IRISExecutor:
2884
2889
  Execute SQL using external IRIS connection with proper async threading
2885
2890
  """
2886
2891
 
2892
+ import threading
2893
+
2894
+ # Shared flag: asyncio side sets this True when wait_for times out so the
2895
+ # sync thread knows to close (evict) the connection instead of recycling it.
2896
+ timed_out = threading.Event()
2897
+
2887
2898
  def _sync_external_execute(captured_sql, captured_params, captured_session_id):
2888
2899
  """Synchronous external IRIS execution in thread pool"""
2889
2900
  sql = captured_sql
@@ -3103,13 +3114,40 @@ class IRISExecutor:
3103
3114
  except Exception:
3104
3115
  pass
3105
3116
  if conn:
3106
- self._return_connection(conn, session_id=session_id)
3117
+ if timed_out.is_set():
3118
+ # Evict — don't return a lock-held connection to the pool
3119
+ try:
3120
+ conn.close()
3121
+ except Exception:
3122
+ pass
3123
+ logger.warning(
3124
+ "Evicted timed-out IRIS connection from pool",
3125
+ session_id=session_id,
3126
+ )
3127
+ else:
3128
+ self._return_connection(conn, session_id=session_id)
3107
3129
 
3108
- # Execute in thread pool to avoid blocking event loop
3130
+ # Execute in thread pool to avoid blocking event loop.
3131
+ # Wrap with a per-query timeout to prevent lock-wait cascades from
3132
+ # exhausting the connection pool (PGWIRE_QUERY_TIMEOUT, default 30s).
3109
3133
  loop = asyncio.get_event_loop()
3110
- return await loop.run_in_executor(
3134
+ future = loop.run_in_executor(
3111
3135
  self._get_executor(session_id), _sync_external_execute, sql, params, session_id
3112
3136
  )
3137
+ try:
3138
+ return await asyncio.wait_for(future, timeout=self.query_timeout)
3139
+ except TimeoutError:
3140
+ timed_out.set() # Signal the sync thread to evict the connection
3141
+ logger.error(
3142
+ "Query timed out — connection will be evicted from pool",
3143
+ sql_preview=sql[:120],
3144
+ timeout_seconds=self.query_timeout,
3145
+ session_id=session_id,
3146
+ )
3147
+ raise RuntimeError(
3148
+ f"Query execution timed out after {self.query_timeout}s. "
3149
+ "The IRIS connection is being evicted to prevent pool exhaustion."
3150
+ )
3113
3151
 
3114
3152
  def _get_iris_connection(self):
3115
3153
  """
@@ -80,6 +80,18 @@ class BackendConfig(BaseModel):
80
80
  description="Connection lifetime in seconds (1 hour default)",
81
81
  )
82
82
 
83
+ # Per-query execution timeout (separate from pool wait timeout)
84
+ query_timeout: float = Field(
85
+ default=30.0,
86
+ ge=1.0,
87
+ le=600.0,
88
+ description=(
89
+ "Maximum seconds a single query may execute before being cancelled. "
90
+ "Prevents lock-wait cascades from exhausting the connection pool. "
91
+ "Set via PGWIRE_QUERY_TIMEOUT env var."
92
+ ),
93
+ )
94
+
83
95
  # Protocol Behavior
84
96
  strict_single_connection: bool = Field(
85
97
  default=False,
@@ -205,6 +217,7 @@ class BackendConfig(BaseModel):
205
217
  ("PGWIRE_POOL_MAX_OVERFLOW", "pool_max_overflow", int),
206
218
  ("PGWIRE_POOL_TIMEOUT", "pool_timeout", int),
207
219
  ("PGWIRE_POOL_RECYCLE", "pool_recycle", int),
220
+ ("PGWIRE_QUERY_TIMEOUT", "query_timeout", float),
208
221
  # Observability
209
222
  ("PGWIRE_ENABLE_OTEL", "enable_otel", _as_bool),
210
223
  ("PGWIRE_OTEL_ENDPOINT", "otel_endpoint", None),
@@ -75,6 +75,7 @@ class PGWireServer:
75
75
  enable_scram: bool = False,
76
76
  connection_pool_size: int = 10,
77
77
  connection_pool_timeout: float = 5.0,
78
+ query_timeout: float = 30.0,
78
79
  enable_query_cache: bool = True,
79
80
  query_cache_size: int = 1000,
80
81
  backend_type: str | None = None,
@@ -122,6 +123,7 @@ class PGWireServer:
122
123
  iris_namespace=iris_namespace,
123
124
  pool_size=connection_pool_size,
124
125
  pool_timeout=int(connection_pool_timeout),
126
+ query_timeout=query_timeout,
125
127
  strict_single_connection=strict_single_connection,
126
128
  )
127
129
 
@@ -300,6 +302,7 @@ def _gather_main_config() -> dict[str, Any]:
300
302
  "ssl_key_path": os.getenv("PGWIRE_SSL_KEY"),
301
303
  "connection_pool_size": int(os.getenv("PGWIRE_POOL_SIZE", "10")),
302
304
  "connection_pool_timeout": float(os.getenv("PGWIRE_POOL_TIMEOUT", "5.0")),
305
+ "query_timeout": float(os.getenv("PGWIRE_QUERY_TIMEOUT", "30.0")),
303
306
  "enable_query_cache": os.getenv("PGWIRE_QUERY_CACHE_ENABLED", "true").lower() == "true",
304
307
  "query_cache_size": int(os.getenv("PGWIRE_QUERY_CACHE_SIZE", "1000")),
305
308
  "debug": os.getenv("PGWIRE_DEBUG", "false").lower() == "true",
@@ -327,6 +330,7 @@ async def main():
327
330
  ssl_key_path=config["ssl_key_path"],
328
331
  connection_pool_size=config["connection_pool_size"],
329
332
  connection_pool_timeout=config["connection_pool_timeout"],
333
+ query_timeout=config["query_timeout"],
330
334
  enable_query_cache=config["enable_query_cache"],
331
335
  query_cache_size=config["query_cache_size"],
332
336
  )
File without changes
File without changes
File without changes
File without changes