iris-pgwire 1.2.33__tar.gz → 1.2.34__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.2.33 → iris_pgwire-1.2.34}/PKG-INFO +1 -1
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/__init__.py +1 -1
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/dbapi_executor.py +510 -25
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/iris_executor.py +93 -87
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/.gitignore +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/CHANGELOG.md +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/LICENSE +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/README.md +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/docs/VECTOR_PARAMETER_BINDING.md +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/examples/BI_TOOLS_SETUP.md +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/examples/async_sqlalchemy_demo.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/examples/bi_tools_demo.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/examples/client_demonstrations.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/examples/client_demos.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/examples/translation_api_demo.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/pyproject.toml +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/auth/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/auth/auth_selector.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/auth/gssapi_auth.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/auth/oauth_bridge.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/auth/scram.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/auth/wallet_credentials.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/backend_selector.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/bulk_executor.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/catalog_functions.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/catalog_router.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/oid_generator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_attrdef.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_class.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_index.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/catalog/pg_type.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/column_validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/config_schema.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/constitutional.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/date_horolog.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/ddl_splitter.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/json_path.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/copy_handler.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/csv_processor.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/dbapi_connection_pool.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/debug_tracer.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/health_checker.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/integratedml.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/iris_constructs.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/iris_log_handler.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/iris_user_management.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/models/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/models/backend_config.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/models/connection_pool_state.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/models/dbapi_connection.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/models/ipm_metadata.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/models/vector_query_request.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/observability.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/performance_monitor.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/protocol.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/__main__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/documentation_validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/security_validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/quality/validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/schema_mapper.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/server.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/api.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/cache.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/confidence_analyzer.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/config.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/debug.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/default_values.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/error_handler.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/identifier_normalizer.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/metrics.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/models.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/normalizer.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/parser.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/performance_monitor.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/refiner.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/statement_filter.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/translator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/sql_translator/validator.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/testing/__init__.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/type_mapping.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/src/iris_pgwire/vector_metrics.py +0 -0
- {iris_pgwire-1.2.33 → iris_pgwire-1.2.34}/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.2.
|
|
3
|
+
Version: 1.2.34
|
|
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.2.
|
|
9
|
+
__version__ = "1.2.34"
|
|
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
|
|
@@ -14,6 +14,7 @@ Contract: contracts/dbapi-executor-contract.md
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
+
import datetime as dt
|
|
17
18
|
import re
|
|
18
19
|
import time
|
|
19
20
|
from typing import Any
|
|
@@ -25,12 +26,43 @@ from iris_pgwire.dbapi_connection_pool import IRISConnectionPool
|
|
|
25
26
|
from iris_pgwire.models.backend_config import BackendConfig
|
|
26
27
|
from iris_pgwire.models.connection_pool_state import ConnectionPoolState
|
|
27
28
|
from iris_pgwire.models.vector_query_request import VectorQueryRequest
|
|
29
|
+
from iris_pgwire.schema_mapper import IRIS_SCHEMA
|
|
28
30
|
from iris_pgwire.sql_translator import SQLPipeline
|
|
29
31
|
from iris_pgwire.sql_translator.parser import get_parser
|
|
30
32
|
|
|
31
33
|
logger = structlog.get_logger(__name__)
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
class MockResult:
|
|
37
|
+
"""Mock result object for RETURNING emulation"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, rows, meta=None):
|
|
40
|
+
self._rows = rows if rows is not None else []
|
|
41
|
+
self._meta = meta
|
|
42
|
+
self.description = meta
|
|
43
|
+
self.rowcount = len(self._rows)
|
|
44
|
+
self._index = 0
|
|
45
|
+
|
|
46
|
+
def __iter__(self):
|
|
47
|
+
return iter(self._rows)
|
|
48
|
+
|
|
49
|
+
def fetchall(self):
|
|
50
|
+
return self._rows
|
|
51
|
+
|
|
52
|
+
def fetchone(self):
|
|
53
|
+
if self._index < len(self._rows):
|
|
54
|
+
row = self._rows[self._index]
|
|
55
|
+
self._index += 1
|
|
56
|
+
return row
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def fetch(self):
|
|
60
|
+
return self._rows
|
|
61
|
+
|
|
62
|
+
def close(self):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
34
66
|
class DBAPIExecutor:
|
|
35
67
|
"""
|
|
36
68
|
Execute SQL queries against IRIS via DBAPI backend.
|
|
@@ -103,11 +135,9 @@ class DBAPIExecutor:
|
|
|
103
135
|
def _convert_value_for_iris(self, value: Any) -> Any:
|
|
104
136
|
"""Helper to convert a single value."""
|
|
105
137
|
if isinstance(value, str):
|
|
106
|
-
#
|
|
107
|
-
#
|
|
138
|
+
# Check for ISO 8601 timestamp: 2026-01-29T21:27:38.111Z
|
|
139
|
+
# or 2026-01-29T21:27:38.111+00:00
|
|
108
140
|
# IRIS rejects the 'T' and 'Z' or offset in %PosixTime/TIMESTAMP
|
|
109
|
-
import re
|
|
110
|
-
|
|
111
141
|
ts_match = re.match(
|
|
112
142
|
r"^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2}(?:\.\d+)?)(?:Z|[+-]\d{2}:?(\d{2})?)?$",
|
|
113
143
|
value,
|
|
@@ -151,21 +181,55 @@ class DBAPIExecutor:
|
|
|
151
181
|
# Acquire connection from pool
|
|
152
182
|
conn_wrapper = await self.pool.acquire()
|
|
153
183
|
|
|
184
|
+
# Detect RETURNING clause
|
|
185
|
+
has_returning = self.has_returning_clause(sql)
|
|
186
|
+
|
|
154
187
|
# Execute query in thread pool (DBAPI is synchronous)
|
|
155
188
|
def execute_in_thread():
|
|
156
189
|
cursor = conn_wrapper.connection.cursor() # type: ignore
|
|
157
190
|
try:
|
|
158
|
-
# Strip trailing semicolon for IRIS compatibility
|
|
159
|
-
clean_sql = sql.strip().rstrip(";")
|
|
160
|
-
|
|
161
191
|
# Feature 034: Apply per-session namespace if set
|
|
162
192
|
if session_id and session_id in self.session_namespaces:
|
|
163
193
|
ns = self.session_namespaces[session_id]
|
|
164
|
-
# In DBAPI, we switch namespace by executing a command if supported,
|
|
165
|
-
# but IRIS DBAPI connection is usually fixed to a namespace.
|
|
166
|
-
# For now, we log it.
|
|
167
194
|
logger.debug(f"Session {session_id} using namespace {ns}")
|
|
168
195
|
|
|
196
|
+
# Handle RETURNING emulation
|
|
197
|
+
if has_returning:
|
|
198
|
+
op, table, cols, where, stripped_sql = self._parse_returning_clause(sql)
|
|
199
|
+
if op and table:
|
|
200
|
+
# Strip trailing semicolon
|
|
201
|
+
clean_sql = stripped_sql.strip().rstrip(";")
|
|
202
|
+
|
|
203
|
+
# For DELETE, we must fetch BEFORE deleting
|
|
204
|
+
delete_rows = []
|
|
205
|
+
delete_meta = None
|
|
206
|
+
if op == "DELETE":
|
|
207
|
+
delete_rows, delete_meta = self._emulate_returning_sync(
|
|
208
|
+
cursor, op, table, cols, where, converted_params, sql
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Execute the main statement
|
|
212
|
+
if converted_params:
|
|
213
|
+
cursor.execute(clean_sql, converted_params)
|
|
214
|
+
else:
|
|
215
|
+
cursor.execute(clean_sql)
|
|
216
|
+
|
|
217
|
+
# Emulate RETURNING result
|
|
218
|
+
if op == "DELETE":
|
|
219
|
+
rows = delete_rows
|
|
220
|
+
columns = delete_meta
|
|
221
|
+
else:
|
|
222
|
+
rows, columns = self._emulate_returning_sync(
|
|
223
|
+
cursor, op, table, cols, where, converted_params, sql
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
row_count = len(rows)
|
|
227
|
+
return rows, columns, row_count
|
|
228
|
+
|
|
229
|
+
# Standard execution path
|
|
230
|
+
# Strip trailing semicolon for IRIS compatibility
|
|
231
|
+
clean_sql = sql.strip().rstrip(";")
|
|
232
|
+
|
|
169
233
|
if converted_params:
|
|
170
234
|
cursor.execute(clean_sql, converted_params)
|
|
171
235
|
else:
|
|
@@ -260,13 +324,8 @@ class DBAPIExecutor:
|
|
|
260
324
|
"""
|
|
261
325
|
Execute SQL with multiple parameter sets for batch operations.
|
|
262
326
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
params_list: List of parameter tuples/lists
|
|
266
|
-
session_id: Optional session identifier
|
|
267
|
-
|
|
268
|
-
Returns:
|
|
269
|
-
Dict with execution results (rows_affected, execution_time_ms, etc.)
|
|
327
|
+
RETURNING SUPPORT: When SQL contains RETURNING clause, executes each statement
|
|
328
|
+
individually and aggregates the returned rows.
|
|
270
329
|
"""
|
|
271
330
|
start_time = time.perf_counter()
|
|
272
331
|
conn_wrapper = None
|
|
@@ -275,6 +334,9 @@ class DBAPIExecutor:
|
|
|
275
334
|
# Translate placeholders ($1 -> ?)
|
|
276
335
|
sql = self._translate_placeholders(sql)
|
|
277
336
|
|
|
337
|
+
# Detect RETURNING clause
|
|
338
|
+
has_returning = self.has_returning_clause(sql)
|
|
339
|
+
|
|
278
340
|
# Acquire connection from pool
|
|
279
341
|
conn_wrapper = await self.pool.acquire()
|
|
280
342
|
|
|
@@ -285,6 +347,38 @@ class DBAPIExecutor:
|
|
|
285
347
|
# Strip trailing semicolon for IRIS compatibility
|
|
286
348
|
clean_sql = sql.strip().rstrip(";")
|
|
287
349
|
|
|
350
|
+
if has_returning:
|
|
351
|
+
op, table, cols, where, stripped_sql = self._parse_returning_clause(sql)
|
|
352
|
+
if op and table:
|
|
353
|
+
all_rows = []
|
|
354
|
+
all_meta = None
|
|
355
|
+
|
|
356
|
+
for params in params_list:
|
|
357
|
+
converted_params = self._convert_params_for_iris(params)
|
|
358
|
+
# For DELETE, capture before
|
|
359
|
+
if op == "DELETE":
|
|
360
|
+
rows, meta = self._emulate_returning_sync(
|
|
361
|
+
cursor, op, table, cols, where, converted_params, sql
|
|
362
|
+
)
|
|
363
|
+
all_rows.extend(rows)
|
|
364
|
+
if not all_meta:
|
|
365
|
+
all_meta = meta
|
|
366
|
+
|
|
367
|
+
# Execute statement
|
|
368
|
+
cursor.execute(stripped_sql.strip().rstrip(";"), converted_params)
|
|
369
|
+
|
|
370
|
+
# For INSERT/UPDATE, capture after
|
|
371
|
+
if op != "DELETE":
|
|
372
|
+
rows, meta = self._emulate_returning_sync(
|
|
373
|
+
cursor, op, table, cols, where, converted_params, sql
|
|
374
|
+
)
|
|
375
|
+
all_rows.extend(rows)
|
|
376
|
+
if not all_meta:
|
|
377
|
+
all_meta = meta
|
|
378
|
+
|
|
379
|
+
return all_rows, all_meta or [], len(params_list)
|
|
380
|
+
|
|
381
|
+
# Standard batch execution
|
|
288
382
|
# Pre-process parameters (e.g. convert lists to IRIS vector strings)
|
|
289
383
|
final_params_list = []
|
|
290
384
|
for p_set in params_list:
|
|
@@ -307,11 +401,11 @@ class DBAPIExecutor:
|
|
|
307
401
|
rows_affected = (
|
|
308
402
|
cursor.rowcount if hasattr(cursor, "rowcount") else len(params_list)
|
|
309
403
|
)
|
|
310
|
-
return rows_affected
|
|
404
|
+
return [], [], rows_affected
|
|
311
405
|
finally:
|
|
312
406
|
cursor.close()
|
|
313
407
|
|
|
314
|
-
rows_affected = await asyncio.to_thread(execute_batch_in_thread)
|
|
408
|
+
rows, columns, rows_affected = await asyncio.to_thread(execute_batch_in_thread)
|
|
315
409
|
|
|
316
410
|
# Record metrics
|
|
317
411
|
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
@@ -332,9 +426,11 @@ class DBAPIExecutor:
|
|
|
332
426
|
"rows_affected": rows_affected,
|
|
333
427
|
"execution_time_ms": elapsed_ms,
|
|
334
428
|
"batch_size": len(params_list),
|
|
335
|
-
"rows":
|
|
336
|
-
"columns":
|
|
337
|
-
"_execution_path":
|
|
429
|
+
"rows": rows,
|
|
430
|
+
"columns": columns,
|
|
431
|
+
"_execution_path": (
|
|
432
|
+
"dbapi_executemany_returning" if has_returning else "dbapi_executemany"
|
|
433
|
+
),
|
|
338
434
|
}
|
|
339
435
|
|
|
340
436
|
except Exception as e:
|
|
@@ -433,14 +529,18 @@ class DBAPIExecutor:
|
|
|
433
529
|
}
|
|
434
530
|
|
|
435
531
|
def has_returning_clause(self, query: str) -> bool:
|
|
436
|
-
"""
|
|
532
|
+
"""
|
|
533
|
+
Check if query has a RETURNING clause.
|
|
534
|
+
"""
|
|
437
535
|
if not query:
|
|
438
536
|
return False
|
|
439
537
|
return bool(re.search(r"\bRETURNING\b", query, re.IGNORECASE | re.DOTALL))
|
|
440
538
|
|
|
441
539
|
def get_returning_columns(self, query: str) -> list[str]:
|
|
442
|
-
"""
|
|
443
|
-
|
|
540
|
+
"""
|
|
541
|
+
Extract column names from RETURNING clause.
|
|
542
|
+
"""
|
|
543
|
+
match = re.search(r"RETURNING\s+(.+?)(?=$|;)", query, re.IGNORECASE | re.DOTALL)
|
|
444
544
|
if not match:
|
|
445
545
|
return []
|
|
446
546
|
cols_str = match.group(1).strip()
|
|
@@ -448,6 +548,356 @@ class DBAPIExecutor:
|
|
|
448
548
|
return ["*"]
|
|
449
549
|
return [c.strip() for c in cols_str.split(",")]
|
|
450
550
|
|
|
551
|
+
def _get_table_columns_from_schema(self, table: str, cursor=None) -> list[str]:
|
|
552
|
+
"""
|
|
553
|
+
Query INFORMATION_SCHEMA.COLUMNS for the given table.
|
|
554
|
+
Returns the list of column names in order.
|
|
555
|
+
"""
|
|
556
|
+
if self.strict_single_connection or cursor is None:
|
|
557
|
+
return []
|
|
558
|
+
try:
|
|
559
|
+
table_clean = table.strip('"').strip("'")
|
|
560
|
+
metadata_sql = f"""
|
|
561
|
+
SELECT COLUMN_NAME
|
|
562
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
563
|
+
WHERE LOWER(TABLE_NAME) = LOWER('{table_clean}')
|
|
564
|
+
AND LOWER(TABLE_SCHEMA) = LOWER('{IRIS_SCHEMA}')
|
|
565
|
+
ORDER BY ORDINAL_POSITION
|
|
566
|
+
"""
|
|
567
|
+
cursor.execute(metadata_sql)
|
|
568
|
+
rows = cursor.fetchall()
|
|
569
|
+
return [row[0] for row in rows]
|
|
570
|
+
except Exception as e:
|
|
571
|
+
logger.debug(f"Failed to get columns from schema for {table}: {e}")
|
|
572
|
+
return []
|
|
573
|
+
|
|
574
|
+
def _get_column_type_from_schema(self, table: str, column: str, cursor=None) -> int | None:
|
|
575
|
+
"""
|
|
576
|
+
Query INFORMATION_SCHEMA.COLUMNS for the given table and column.
|
|
577
|
+
Returns the PostgreSQL type OID.
|
|
578
|
+
"""
|
|
579
|
+
if self.strict_single_connection or cursor is None:
|
|
580
|
+
return None
|
|
581
|
+
try:
|
|
582
|
+
table_clean = table.strip('"').strip("'")
|
|
583
|
+
column_clean = column.strip('"').strip("'")
|
|
584
|
+
metadata_sql = f"""
|
|
585
|
+
SELECT DATA_TYPE
|
|
586
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
587
|
+
WHERE LOWER(TABLE_NAME) = LOWER('{table_clean}')
|
|
588
|
+
AND LOWER(COLUMN_NAME) = LOWER('{column_clean}')
|
|
589
|
+
AND LOWER(TABLE_SCHEMA) = LOWER('{IRIS_SCHEMA}')
|
|
590
|
+
"""
|
|
591
|
+
cursor.execute(metadata_sql)
|
|
592
|
+
row = cursor.fetchone()
|
|
593
|
+
if row:
|
|
594
|
+
iris_type = row[0]
|
|
595
|
+
return self._map_iris_type_to_oid(iris_type)
|
|
596
|
+
except Exception as e:
|
|
597
|
+
logger.debug(f"Failed to get type from schema for {table}.{column}: {e}")
|
|
598
|
+
return None
|
|
599
|
+
|
|
600
|
+
def _infer_type_from_value(self, value, column_name: str | None = None) -> int:
|
|
601
|
+
"""
|
|
602
|
+
Infer PostgreSQL type OID from Python value
|
|
603
|
+
"""
|
|
604
|
+
from decimal import Decimal
|
|
605
|
+
|
|
606
|
+
if value is None:
|
|
607
|
+
return 1043 # VARCHAR
|
|
608
|
+
elif isinstance(value, bool):
|
|
609
|
+
return 16 # BOOL
|
|
610
|
+
elif isinstance(value, int):
|
|
611
|
+
if column_name and any(k in column_name.lower() for k in ("id", "key")):
|
|
612
|
+
return 20 # BIGINT
|
|
613
|
+
return 23 # INTEGER
|
|
614
|
+
elif isinstance(value, float):
|
|
615
|
+
return 701 # FLOAT8
|
|
616
|
+
elif isinstance(value, Decimal):
|
|
617
|
+
return 1700 # NUMERIC
|
|
618
|
+
elif isinstance(value, dt.datetime):
|
|
619
|
+
return 1114
|
|
620
|
+
elif isinstance(value, dt.date):
|
|
621
|
+
return 1082
|
|
622
|
+
elif isinstance(value, str):
|
|
623
|
+
return 1043 # VARCHAR
|
|
624
|
+
else:
|
|
625
|
+
return 1043
|
|
626
|
+
|
|
627
|
+
def _serialize_value(self, value: Any, type_oid: int) -> Any:
|
|
628
|
+
"""
|
|
629
|
+
Robust value serialization for PostgreSQL wire protocol compatibility.
|
|
630
|
+
"""
|
|
631
|
+
if value is None:
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
if type_oid == 1114: # TIMESTAMP
|
|
635
|
+
if isinstance(value, dt.datetime):
|
|
636
|
+
return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
637
|
+
elif isinstance(value, str):
|
|
638
|
+
return value # Already a string
|
|
639
|
+
|
|
640
|
+
return value
|
|
641
|
+
|
|
642
|
+
def _parse_returning_clause(
|
|
643
|
+
self, sql: str
|
|
644
|
+
) -> tuple[str | None, str | None, Any, str | None, str]:
|
|
645
|
+
"""
|
|
646
|
+
Parse RETURNING clause from SQL and return metadata.
|
|
647
|
+
Returns: (operation, table, columns, where_clause, stripped_sql)
|
|
648
|
+
"""
|
|
649
|
+
returning_operation = None
|
|
650
|
+
returning_table = None
|
|
651
|
+
returning_columns = None
|
|
652
|
+
returning_where_clause = None
|
|
653
|
+
|
|
654
|
+
returning_pattern = r"\s+RETURNING\s+(.*?)($|;)"
|
|
655
|
+
returning_match = re.search(returning_pattern, sql, re.IGNORECASE | re.DOTALL)
|
|
656
|
+
|
|
657
|
+
if not returning_match:
|
|
658
|
+
return None, None, None, None, sql
|
|
659
|
+
|
|
660
|
+
returning_clause = returning_match.group(1).strip()
|
|
661
|
+
|
|
662
|
+
if returning_clause == "*":
|
|
663
|
+
returning_columns = "*"
|
|
664
|
+
else:
|
|
665
|
+
# Better column parsing that preserves expressions and aliases
|
|
666
|
+
# Split by commas but respect parentheses
|
|
667
|
+
returning_columns = []
|
|
668
|
+
current_col = ""
|
|
669
|
+
depth = 0
|
|
670
|
+
for char in returning_clause:
|
|
671
|
+
if char == "(":
|
|
672
|
+
depth += 1
|
|
673
|
+
elif char == ")":
|
|
674
|
+
depth -= 1
|
|
675
|
+
|
|
676
|
+
if char == "," and depth == 0:
|
|
677
|
+
col = current_col.strip()
|
|
678
|
+
# Extract last part of identifier if it's schema-qualified
|
|
679
|
+
# e.g. public.users.id -> id, or "public"."users"."id" -> id
|
|
680
|
+
col_match = re.search(r'"?(\w+)"?\s*$', col)
|
|
681
|
+
if col_match:
|
|
682
|
+
returning_columns.append(col_match.group(1).lower())
|
|
683
|
+
else:
|
|
684
|
+
returning_columns.append(col.lower())
|
|
685
|
+
current_col = ""
|
|
686
|
+
else:
|
|
687
|
+
current_col += char
|
|
688
|
+
if current_col.strip():
|
|
689
|
+
col = current_col.strip()
|
|
690
|
+
col_match = re.search(r'"?(\w+)"?\s*$', col)
|
|
691
|
+
if col_match:
|
|
692
|
+
returning_columns.append(col_match.group(1).lower())
|
|
693
|
+
else:
|
|
694
|
+
returning_columns.append(col.lower())
|
|
695
|
+
|
|
696
|
+
sql_upper = sql.upper().strip()
|
|
697
|
+
# Robust table extraction regex for all operations
|
|
698
|
+
table_regex = r'(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(?:(?:"?\w+"?)\s*\.\s*)*"?(\w+)"?'
|
|
699
|
+
table_match = re.search(table_regex, sql, re.IGNORECASE)
|
|
700
|
+
if table_match:
|
|
701
|
+
returning_table = table_match.group(1).upper()
|
|
702
|
+
|
|
703
|
+
if sql_upper.startswith("INSERT"):
|
|
704
|
+
returning_operation = "INSERT"
|
|
705
|
+
elif sql_upper.startswith("UPDATE"):
|
|
706
|
+
returning_operation = "UPDATE"
|
|
707
|
+
where_match = re.search(
|
|
708
|
+
r"\bWHERE\s+(.+?)\s+RETURNING\b",
|
|
709
|
+
sql,
|
|
710
|
+
re.IGNORECASE | re.DOTALL,
|
|
711
|
+
)
|
|
712
|
+
if where_match:
|
|
713
|
+
returning_where_clause = where_match.group(1).strip()
|
|
714
|
+
elif sql_upper.startswith("DELETE"):
|
|
715
|
+
returning_operation = "DELETE"
|
|
716
|
+
where_match = re.search(
|
|
717
|
+
r"\bWHERE\s+(.+?)\s+RETURNING\b",
|
|
718
|
+
sql,
|
|
719
|
+
re.IGNORECASE | re.DOTALL,
|
|
720
|
+
)
|
|
721
|
+
if where_match:
|
|
722
|
+
returning_where_clause = where_match.group(1).strip()
|
|
723
|
+
|
|
724
|
+
stripped_sql = re.sub(
|
|
725
|
+
r"\s+RETURNING\s+.*?(?=$|;)",
|
|
726
|
+
"",
|
|
727
|
+
sql,
|
|
728
|
+
flags=re.IGNORECASE | re.DOTALL,
|
|
729
|
+
count=1,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
returning_operation,
|
|
734
|
+
returning_table,
|
|
735
|
+
returning_columns,
|
|
736
|
+
returning_where_clause,
|
|
737
|
+
stripped_sql,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def _expand_select_star(self, sql: str, expected_columns: int, cursor=None) -> list[str] | None:
|
|
741
|
+
"""
|
|
742
|
+
Expand SELECT * or RETURNING * into explicit column names using INFORMATION_SCHEMA.
|
|
743
|
+
"""
|
|
744
|
+
try:
|
|
745
|
+
table_name = None
|
|
746
|
+
sql_upper = sql.upper()
|
|
747
|
+
|
|
748
|
+
if "RETURNING" in sql_upper:
|
|
749
|
+
table_regex = (
|
|
750
|
+
r'(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(?:(?:"?\w+"?)\s*\.\s*)*"?(\w+)"?'
|
|
751
|
+
)
|
|
752
|
+
table_match = re.search(table_regex, sql, re.IGNORECASE)
|
|
753
|
+
if table_match:
|
|
754
|
+
table_name = table_match.group(1)
|
|
755
|
+
else:
|
|
756
|
+
from_match = re.search(r"FROM\s+([^\s,;()]+)", sql, re.IGNORECASE)
|
|
757
|
+
if from_match:
|
|
758
|
+
table_name = from_match.group(1)
|
|
759
|
+
|
|
760
|
+
if table_name:
|
|
761
|
+
if "." in table_name:
|
|
762
|
+
table_name = table_name.split(".")[-1]
|
|
763
|
+
table_name = table_name.strip('"').strip("'")
|
|
764
|
+
|
|
765
|
+
schema_columns = self._get_table_columns_from_schema(table_name, cursor)
|
|
766
|
+
if schema_columns:
|
|
767
|
+
if expected_columns == 0 or len(schema_columns) == expected_columns:
|
|
768
|
+
return schema_columns
|
|
769
|
+
return None
|
|
770
|
+
except Exception as e:
|
|
771
|
+
logger.debug(f"Failed to expand SELECT *: {e}")
|
|
772
|
+
return None
|
|
773
|
+
|
|
774
|
+
def _extract_insert_id_from_sql(
|
|
775
|
+
self, sql: str, params: list | None, session_id: str | None = None
|
|
776
|
+
) -> tuple[str | None, Any]:
|
|
777
|
+
"""
|
|
778
|
+
Extract the ID value from an INSERT statement.
|
|
779
|
+
"""
|
|
780
|
+
col_match = re.search(r"INSERT\s+INTO\s+[^\s(]+\s*\(\s*([^)]+)\s*\)", sql, re.IGNORECASE)
|
|
781
|
+
if not col_match:
|
|
782
|
+
return None, None
|
|
783
|
+
|
|
784
|
+
columns_str = col_match.group(1)
|
|
785
|
+
columns = [c.strip().strip('"').strip("'").lower() for c in columns_str.split(",")]
|
|
786
|
+
|
|
787
|
+
id_col_names = ["id", "uuid", "_id"]
|
|
788
|
+
id_col_idx = None
|
|
789
|
+
id_col_name = None
|
|
790
|
+
for i, col in enumerate(columns):
|
|
791
|
+
if col in id_col_names:
|
|
792
|
+
id_col_idx = i
|
|
793
|
+
id_col_name = col
|
|
794
|
+
break
|
|
795
|
+
|
|
796
|
+
if id_col_idx is None:
|
|
797
|
+
return None, None
|
|
798
|
+
|
|
799
|
+
if params and len(params) > id_col_idx:
|
|
800
|
+
return id_col_name, params[id_col_idx]
|
|
801
|
+
|
|
802
|
+
return None, None
|
|
803
|
+
|
|
804
|
+
def _emulate_returning_sync(
|
|
805
|
+
self,
|
|
806
|
+
cursor,
|
|
807
|
+
operation: str,
|
|
808
|
+
table: str,
|
|
809
|
+
columns: list[str] | str,
|
|
810
|
+
where_clause: str | None,
|
|
811
|
+
params: list | None,
|
|
812
|
+
original_sql: str | None = None,
|
|
813
|
+
) -> tuple[list[Any], Any]:
|
|
814
|
+
"""
|
|
815
|
+
Synchronous emulation of RETURNING clause.
|
|
816
|
+
"""
|
|
817
|
+
table_normalized = table.upper() if table else table
|
|
818
|
+
if columns == "*":
|
|
819
|
+
# Expand * using table schema
|
|
820
|
+
expanded_cols = self._get_table_columns_from_schema(table_normalized, cursor)
|
|
821
|
+
if expanded_cols:
|
|
822
|
+
col_list = ", ".join([f'"{col}"' for col in expanded_cols])
|
|
823
|
+
columns = expanded_cols # Update columns for metadata generation
|
|
824
|
+
else:
|
|
825
|
+
col_list = "*"
|
|
826
|
+
else:
|
|
827
|
+
col_list = ", ".join([f'"{col}"' for col in columns])
|
|
828
|
+
|
|
829
|
+
rows = []
|
|
830
|
+
meta = None
|
|
831
|
+
|
|
832
|
+
try:
|
|
833
|
+
if operation == "INSERT":
|
|
834
|
+
# Method 1: LAST_IDENTITY()
|
|
835
|
+
cursor.execute("SELECT LAST_IDENTITY()")
|
|
836
|
+
id_row = cursor.fetchone()
|
|
837
|
+
last_id = id_row[0] if id_row else None
|
|
838
|
+
|
|
839
|
+
if last_id:
|
|
840
|
+
cursor.execute(
|
|
841
|
+
f'SELECT {col_list} FROM {IRIS_SCHEMA}."{table_normalized}" WHERE %ID = ?',
|
|
842
|
+
(last_id,),
|
|
843
|
+
)
|
|
844
|
+
rows = cursor.fetchall()
|
|
845
|
+
meta = cursor.description
|
|
846
|
+
|
|
847
|
+
# Method 2: Extract from SQL if still no rows
|
|
848
|
+
if not rows and original_sql:
|
|
849
|
+
id_col_name, id_value = self._extract_insert_id_from_sql(original_sql, params)
|
|
850
|
+
if id_col_name and id_value:
|
|
851
|
+
cursor.execute(
|
|
852
|
+
f'SELECT {col_list} FROM {IRIS_SCHEMA}."{table_normalized}" WHERE "{id_col_name}" = ?',
|
|
853
|
+
(id_value,),
|
|
854
|
+
)
|
|
855
|
+
rows = cursor.fetchall()
|
|
856
|
+
meta = cursor.description
|
|
857
|
+
|
|
858
|
+
elif operation in ("UPDATE", "DELETE"):
|
|
859
|
+
if where_clause:
|
|
860
|
+
# Translate schema references in WHERE clause
|
|
861
|
+
translated_where = re.sub(
|
|
862
|
+
r'"public"\s*\.\s*"(\w+)"',
|
|
863
|
+
rf'{IRIS_SCHEMA}."\1"',
|
|
864
|
+
where_clause,
|
|
865
|
+
flags=re.IGNORECASE,
|
|
866
|
+
)
|
|
867
|
+
translated_where = re.sub(
|
|
868
|
+
r'\bpublic\s*\.\s*"(\w+)"',
|
|
869
|
+
rf'{IRIS_SCHEMA}."\1"',
|
|
870
|
+
translated_where,
|
|
871
|
+
flags=re.IGNORECASE,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
# Very basic where clause parameter extraction
|
|
875
|
+
where_param_count = translated_where.count("?")
|
|
876
|
+
where_params = (
|
|
877
|
+
params[-where_param_count:] if params and where_param_count > 0 else None
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
cursor.execute(
|
|
881
|
+
f'SELECT {col_list} FROM {IRIS_SCHEMA}."{table_normalized}" WHERE {translated_where}',
|
|
882
|
+
where_params or (),
|
|
883
|
+
)
|
|
884
|
+
rows = cursor.fetchall()
|
|
885
|
+
meta = cursor.description
|
|
886
|
+
|
|
887
|
+
# Build metadata if needed
|
|
888
|
+
if meta and not any(isinstance(m, dict) and "type_oid" in m for m in meta):
|
|
889
|
+
new_meta = []
|
|
890
|
+
for i, desc in enumerate(meta):
|
|
891
|
+
col_name = desc[0]
|
|
892
|
+
col_oid = self._map_dbapi_type_to_oid(desc[1])
|
|
893
|
+
new_meta.append({"name": col_name, "type_oid": col_oid, "format_code": 0})
|
|
894
|
+
meta = new_meta
|
|
895
|
+
|
|
896
|
+
except Exception as e:
|
|
897
|
+
logger.error(f"RETURNING emulation failed: {e}")
|
|
898
|
+
|
|
899
|
+
return rows, meta
|
|
900
|
+
|
|
451
901
|
def _map_dbapi_type_to_oid(self, dbapi_type: Any) -> int:
|
|
452
902
|
"""Map DBAPI type to PostgreSQL OID."""
|
|
453
903
|
# Simple mapping for now, can be expanded
|
|
@@ -462,6 +912,41 @@ class DBAPIExecutor:
|
|
|
462
912
|
return 1114
|
|
463
913
|
return 1043 # Default to VARCHAR
|
|
464
914
|
|
|
915
|
+
def _map_iris_type_to_oid(self, iris_type: str) -> int:
|
|
916
|
+
"""
|
|
917
|
+
Map IRIS data type to PostgreSQL type OID.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
iris_type: IRIS data type (e.g., 'INT', 'VARCHAR', 'DATE')
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
PostgreSQL type OID
|
|
924
|
+
"""
|
|
925
|
+
type_map = {
|
|
926
|
+
"INT": 23, # int4
|
|
927
|
+
"INTEGER": 23, # int4
|
|
928
|
+
"BIGINT": 20, # int8
|
|
929
|
+
"SMALLINT": 21, # int2
|
|
930
|
+
"VARCHAR": 1043, # varchar
|
|
931
|
+
"CHAR": 1042, # char
|
|
932
|
+
"TEXT": 25, # text
|
|
933
|
+
"DATE": 1082, # date
|
|
934
|
+
"TIME": 1083, # time
|
|
935
|
+
"TIMESTAMP": 1114, # timestamp
|
|
936
|
+
"DOUBLE": 701, # float8
|
|
937
|
+
"FLOAT": 701, # float8
|
|
938
|
+
"NUMERIC": 1700, # numeric
|
|
939
|
+
"DECIMAL": 1700, # numeric
|
|
940
|
+
"BIT": 1560, # bit
|
|
941
|
+
"BOOLEAN": 16, # bool
|
|
942
|
+
"VARBINARY": 17, # bytea
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
# Normalize type name (remove size, etc.)
|
|
946
|
+
normalized_type = iris_type.upper().split("(")[0].strip()
|
|
947
|
+
|
|
948
|
+
return type_map.get(normalized_type, 1043) # Default to VARCHAR (OID 1043)
|
|
949
|
+
|
|
465
950
|
def _determine_command_tag(self, sql: str, row_count: int) -> str:
|
|
466
951
|
"""Determine PostgreSQL command tag from SQL"""
|
|
467
952
|
sql_clean = sql.strip().upper()
|