iris-pgwire 1.2.33__py3-none-any.whl → 1.2.34__py3-none-any.whl
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/__init__.py +1 -1
- iris_pgwire/dbapi_executor.py +510 -25
- iris_pgwire/iris_executor.py +93 -87
- {iris_pgwire-1.2.33.dist-info → iris_pgwire-1.2.34.dist-info}/METADATA +1 -1
- {iris_pgwire-1.2.33.dist-info → iris_pgwire-1.2.34.dist-info}/RECORD +8 -8
- {iris_pgwire-1.2.33.dist-info → iris_pgwire-1.2.34.dist-info}/WHEEL +0 -0
- {iris_pgwire-1.2.33.dist-info → iris_pgwire-1.2.34.dist-info}/entry_points.txt +0 -0
- {iris_pgwire-1.2.33.dist-info → iris_pgwire-1.2.34.dist-info}/licenses/LICENSE +0 -0
iris_pgwire/__init__.py
CHANGED
|
@@ -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
|
iris_pgwire/dbapi_executor.py
CHANGED
|
@@ -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()
|
iris_pgwire/iris_executor.py
CHANGED
|
@@ -1623,41 +1623,49 @@ class IRISExecutor:
|
|
|
1623
1623
|
if returning_clause == "*":
|
|
1624
1624
|
returning_columns = "*"
|
|
1625
1625
|
else:
|
|
1626
|
-
#
|
|
1627
|
-
#
|
|
1628
|
-
raw_cols = [c.strip() for c in returning_clause.split(",")]
|
|
1626
|
+
# Better column parsing that preserves expressions and aliases
|
|
1627
|
+
# Split by commas but respect parentheses
|
|
1629
1628
|
returning_columns = []
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if
|
|
1629
|
+
current_col = ""
|
|
1630
|
+
depth = 0
|
|
1631
|
+
for char in returning_clause:
|
|
1632
|
+
if char == "(":
|
|
1633
|
+
depth += 1
|
|
1634
|
+
elif char == ")":
|
|
1635
|
+
depth -= 1
|
|
1636
|
+
|
|
1637
|
+
if char == "," and depth == 0:
|
|
1638
|
+
col = current_col.strip()
|
|
1639
|
+
# Extract last part of identifier if it's schema-qualified
|
|
1640
|
+
# e.g. public.users.id -> id, or "public"."users"."id" -> id
|
|
1634
1641
|
col_match = re.search(r'"?(\w+)"?\s*$', col)
|
|
1642
|
+
if col_match:
|
|
1643
|
+
returning_columns.append(col_match.group(1).lower())
|
|
1644
|
+
else:
|
|
1645
|
+
returning_columns.append(col.lower())
|
|
1646
|
+
current_col = ""
|
|
1635
1647
|
else:
|
|
1636
|
-
|
|
1637
|
-
|
|
1648
|
+
current_col += char
|
|
1649
|
+
if current_col.strip():
|
|
1650
|
+
col = current_col.strip()
|
|
1651
|
+
col_match = re.search(r'"?(\w+)"?\s*$', col)
|
|
1638
1652
|
if col_match:
|
|
1639
1653
|
returning_columns.append(col_match.group(1).lower())
|
|
1654
|
+
else:
|
|
1655
|
+
returning_columns.append(col.lower())
|
|
1640
1656
|
|
|
1641
1657
|
# Determine operation type and extract table/where clause
|
|
1642
1658
|
sql_upper = sql.upper().strip()
|
|
1659
|
+
# Robust table extraction regex for all operations
|
|
1660
|
+
table_regex = r'(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(?:(?:"?\w+"?)\s*\.\s*)*"?(\w+)"?'
|
|
1661
|
+
table_match = re.search(table_regex, sql, re.IGNORECASE)
|
|
1662
|
+
if table_match:
|
|
1663
|
+
returning_table = table_match.group(1).upper()
|
|
1664
|
+
|
|
1643
1665
|
if sql_upper.startswith("INSERT"):
|
|
1644
1666
|
returning_operation = "INSERT"
|
|
1645
|
-
table_match = re.search(
|
|
1646
|
-
rf'INSERT\s+INTO\s+(?:{re.escape(IRIS_SCHEMA)}\s*\.\s*)?"?(\w+)"?',
|
|
1647
|
-
sql,
|
|
1648
|
-
re.IGNORECASE,
|
|
1649
|
-
)
|
|
1650
|
-
if table_match:
|
|
1651
|
-
returning_table = table_match.group(1)
|
|
1652
1667
|
elif sql_upper.startswith("UPDATE"):
|
|
1653
1668
|
returning_operation = "UPDATE"
|
|
1654
|
-
table_match = re.search(
|
|
1655
|
-
rf'UPDATE\s+(?:{re.escape(IRIS_SCHEMA)}\s*\.\s*)?"?(\w+)"?',
|
|
1656
|
-
sql,
|
|
1657
|
-
re.IGNORECASE,
|
|
1658
|
-
)
|
|
1659
|
-
if table_match:
|
|
1660
|
-
returning_table = table_match.group(1)
|
|
1661
1669
|
# Extract WHERE clause (everything between WHERE and RETURNING)
|
|
1662
1670
|
where_match = re.search(
|
|
1663
1671
|
r"\bWHERE\s+(.+?)\s+RETURNING\b",
|
|
@@ -1668,13 +1676,6 @@ class IRISExecutor:
|
|
|
1668
1676
|
returning_where_clause = where_match.group(1).strip()
|
|
1669
1677
|
elif sql_upper.startswith("DELETE"):
|
|
1670
1678
|
returning_operation = "DELETE"
|
|
1671
|
-
table_match = re.search(
|
|
1672
|
-
rf'DELETE\s+FROM\s+(?:{re.escape(IRIS_SCHEMA)}\s*\.\s*)?"?(\w+)"?',
|
|
1673
|
-
sql,
|
|
1674
|
-
re.IGNORECASE,
|
|
1675
|
-
)
|
|
1676
|
-
if table_match:
|
|
1677
|
-
returning_table = table_match.group(1)
|
|
1678
1679
|
# Extract WHERE clause
|
|
1679
1680
|
where_match = re.search(
|
|
1680
1681
|
r"\bWHERE\s+(.+?)\s+RETURNING\b",
|
|
@@ -1684,13 +1685,15 @@ class IRISExecutor:
|
|
|
1684
1685
|
if where_match:
|
|
1685
1686
|
returning_where_clause = where_match.group(1).strip()
|
|
1686
1687
|
|
|
1688
|
+
# Strip RETURNING clause from SQL for execution
|
|
1689
|
+
# Use a non-greedy match to avoid stripping important parts if multiple statements exist
|
|
1687
1690
|
stripped_sql = re.sub(
|
|
1688
1691
|
r"\s+RETURNING\s+.*?(?=$|;)",
|
|
1689
1692
|
"",
|
|
1690
1693
|
sql,
|
|
1691
1694
|
flags=re.IGNORECASE | re.DOTALL,
|
|
1692
1695
|
count=1,
|
|
1693
|
-
)
|
|
1696
|
+
).strip()
|
|
1694
1697
|
|
|
1695
1698
|
return (
|
|
1696
1699
|
returning_operation,
|
|
@@ -1805,14 +1808,31 @@ class IRISExecutor:
|
|
|
1805
1808
|
import re
|
|
1806
1809
|
|
|
1807
1810
|
# CRITICAL FIX: Normalize table name to UPPERCASE for IRIS compatibility
|
|
1808
|
-
# IRIS stores table names in uppercase in INFORMATION_SCHEMA
|
|
1809
1811
|
table_normalized = table.upper() if table else table
|
|
1810
1812
|
|
|
1811
1813
|
# Handle columns as list or '*'
|
|
1812
1814
|
if columns == "*":
|
|
1813
|
-
|
|
1815
|
+
# Expand * early to get real column names
|
|
1816
|
+
expanded_cols = self._expand_select_star(
|
|
1817
|
+
f"SELECT * FROM {IRIS_SCHEMA}.{table_normalized}", 0, session_id=session_id
|
|
1818
|
+
)
|
|
1819
|
+
if expanded_cols:
|
|
1820
|
+
columns = expanded_cols
|
|
1821
|
+
col_list = ", ".join([f'"{col}"' for col in columns])
|
|
1822
|
+
else:
|
|
1823
|
+
col_list = "*"
|
|
1814
1824
|
else:
|
|
1815
|
-
|
|
1825
|
+
# columns is a list of expressions/names. Preserve them but quote simple identifiers.
|
|
1826
|
+
processed_cols = []
|
|
1827
|
+
for col in columns:
|
|
1828
|
+
if re.match(r"^\"?\w+\"?$", col):
|
|
1829
|
+
# Simple identifier - quote it
|
|
1830
|
+
clean_col = col.strip('"')
|
|
1831
|
+
processed_cols.append(f'"{clean_col}"')
|
|
1832
|
+
else:
|
|
1833
|
+
# Expression - leave as is
|
|
1834
|
+
processed_cols.append(col)
|
|
1835
|
+
col_list = ", ".join(processed_cols)
|
|
1816
1836
|
|
|
1817
1837
|
rows = []
|
|
1818
1838
|
meta = None
|
|
@@ -1855,7 +1875,7 @@ class IRISExecutor:
|
|
|
1855
1875
|
f'SELECT {col_list} FROM {IRIS_SCHEMA}."{table_normalized}" WHERE %ID = ?',
|
|
1856
1876
|
[last_id],
|
|
1857
1877
|
)
|
|
1858
|
-
if not rows and columns
|
|
1878
|
+
if not rows and isinstance(columns, list):
|
|
1859
1879
|
id_cols = [
|
|
1860
1880
|
c
|
|
1861
1881
|
for c in columns
|
|
@@ -1929,42 +1949,31 @@ class IRISExecutor:
|
|
|
1929
1949
|
)
|
|
1930
1950
|
rows, meta = _fetch_results(select_sql, where_params)
|
|
1931
1951
|
|
|
1932
|
-
#
|
|
1952
|
+
# Build/Fix metadata
|
|
1933
1953
|
if meta is None or not any("type_oid" in c for c in meta if isinstance(c, dict)):
|
|
1934
|
-
if columns
|
|
1935
|
-
# For SELECT *, expand from schema
|
|
1936
|
-
col_names = self._expand_select_star(
|
|
1937
|
-
f"RETURNING * FROM {IRIS_SCHEMA}.{table}", 0, session_id=session_id
|
|
1938
|
-
)
|
|
1939
|
-
if col_names:
|
|
1940
|
-
new_meta = []
|
|
1941
|
-
for col in col_names:
|
|
1942
|
-
col_oid = self._get_column_type_from_schema(
|
|
1943
|
-
table, col, session_id=session_id
|
|
1944
|
-
)
|
|
1945
|
-
new_meta.append(
|
|
1946
|
-
{
|
|
1947
|
-
"name": col,
|
|
1948
|
-
"type_oid": col_oid or 1043,
|
|
1949
|
-
"type_size": -1,
|
|
1950
|
-
"type_modifier": -1,
|
|
1951
|
-
"format_code": 0,
|
|
1952
|
-
}
|
|
1953
|
-
)
|
|
1954
|
-
meta = new_meta
|
|
1955
|
-
else:
|
|
1954
|
+
if isinstance(columns, list):
|
|
1956
1955
|
new_meta = []
|
|
1957
1956
|
for i, col in enumerate(columns):
|
|
1957
|
+
# Extract alias or column name
|
|
1958
|
+
col_name = col
|
|
1959
|
+
alias_match = re.search(r"\s+AS\s+\"?(\w+)\"?$", col, re.IGNORECASE)
|
|
1960
|
+
if alias_match:
|
|
1961
|
+
col_name = alias_match.group(1)
|
|
1962
|
+
else:
|
|
1963
|
+
col_name = col_name.strip('"')
|
|
1964
|
+
if "." in col_name:
|
|
1965
|
+
col_name = col_name.split(".")[-1]
|
|
1966
|
+
|
|
1958
1967
|
col_oid = self._get_column_type_from_schema(
|
|
1959
|
-
table,
|
|
1968
|
+
table, col_name, session_id=session_id
|
|
1960
1969
|
)
|
|
1961
1970
|
if col_oid is None and rows:
|
|
1962
1971
|
# Fallback to inference from value
|
|
1963
|
-
col_oid = self._infer_type_from_value(rows[0][i],
|
|
1972
|
+
col_oid = self._infer_type_from_value(rows[0][i], col_name)
|
|
1964
1973
|
|
|
1965
1974
|
new_meta.append(
|
|
1966
1975
|
{
|
|
1967
|
-
"name":
|
|
1976
|
+
"name": col_name,
|
|
1968
1977
|
"type_oid": col_oid or 1043,
|
|
1969
1978
|
"type_size": -1,
|
|
1970
1979
|
"type_modifier": -1,
|
|
@@ -1972,6 +1981,9 @@ class IRISExecutor:
|
|
|
1972
1981
|
}
|
|
1973
1982
|
)
|
|
1974
1983
|
meta = new_meta
|
|
1984
|
+
else:
|
|
1985
|
+
# columns is '*' and expansion failed
|
|
1986
|
+
pass
|
|
1975
1987
|
|
|
1976
1988
|
except Exception as e:
|
|
1977
1989
|
logger.error(f"RETURNING emulation failed for {operation}", error=str(e))
|
|
@@ -2304,6 +2316,7 @@ class IRISExecutor:
|
|
|
2304
2316
|
returning_where_clause,
|
|
2305
2317
|
optimized_params,
|
|
2306
2318
|
is_embedded=True,
|
|
2319
|
+
session_id=session_id,
|
|
2307
2320
|
original_sql=sql, # Pass original SQL for UUID extraction
|
|
2308
2321
|
)
|
|
2309
2322
|
result = MockResult(rows, meta)
|
|
@@ -2324,25 +2337,18 @@ class IRISExecutor:
|
|
|
2324
2337
|
# Get original IRIS column name
|
|
2325
2338
|
iris_col_name = col_info.get("name", "")
|
|
2326
2339
|
iris_type = col_info.get("type", "VARCHAR")
|
|
2340
|
+
precomputed_oid = col_info.get("type_oid")
|
|
2327
2341
|
|
|
2328
2342
|
# CRITICAL: Normalize IRIS column names to PostgreSQL conventions
|
|
2329
|
-
# IRIS generates HostVar_1, Expression_1, Aggregate_1 for unnamed columns
|
|
2330
|
-
# PostgreSQL uses ?column?, type names (int4), or function names (count)
|
|
2331
2343
|
col_name = self._normalize_iris_column_name(
|
|
2332
2344
|
iris_col_name, optimized_sql, iris_type
|
|
2333
2345
|
)
|
|
2334
2346
|
|
|
2335
|
-
# DEBUG: Log IRIS type for arithmetic expressions
|
|
2336
|
-
logger.info(
|
|
2337
|
-
"🔍 IRIS metadata type discovery",
|
|
2338
|
-
original_column_name=iris_col_name,
|
|
2339
|
-
normalized_column_name=col_name,
|
|
2340
|
-
iris_type=iris_type,
|
|
2341
|
-
col_info=col_info,
|
|
2342
|
-
)
|
|
2343
|
-
|
|
2344
2347
|
# Get PostgreSQL type OID
|
|
2345
|
-
|
|
2348
|
+
if precomputed_oid is not None:
|
|
2349
|
+
type_oid = precomputed_oid
|
|
2350
|
+
else:
|
|
2351
|
+
type_oid = self._iris_type_to_pg_oid(iris_type)
|
|
2346
2352
|
|
|
2347
2353
|
# CRITICAL FIX: IRIS type code 2 means NUMERIC, but for decimal literals
|
|
2348
2354
|
|
|
@@ -3094,6 +3100,13 @@ class IRISExecutor:
|
|
|
3094
3100
|
connection=conn,
|
|
3095
3101
|
)
|
|
3096
3102
|
|
|
3103
|
+
# Commit for non-SELECT statements to ensure visibility for emulation and durability
|
|
3104
|
+
if not statements[-1].upper().strip().startswith("SELECT"):
|
|
3105
|
+
try:
|
|
3106
|
+
conn.commit()
|
|
3107
|
+
except Exception as commit_err:
|
|
3108
|
+
logger.warning(f"Failed to commit {statements[-1][:50]}: {commit_err}")
|
|
3109
|
+
|
|
3097
3110
|
# RETURNING emulation
|
|
3098
3111
|
if returning_operation and returning_columns:
|
|
3099
3112
|
if returning_operation == "DELETE":
|
|
@@ -3109,6 +3122,7 @@ class IRISExecutor:
|
|
|
3109
3122
|
optimized_params,
|
|
3110
3123
|
is_embedded=False,
|
|
3111
3124
|
connection=conn,
|
|
3125
|
+
session_id=session_id,
|
|
3112
3126
|
original_sql=sql, # Pass original SQL for UUID extraction
|
|
3113
3127
|
)
|
|
3114
3128
|
cursor = MockResult(rows, meta)
|
|
@@ -3564,20 +3578,12 @@ class IRISExecutor:
|
|
|
3564
3578
|
|
|
3565
3579
|
if "RETURNING" in sql_upper:
|
|
3566
3580
|
# For INSERT/UPDATE/DELETE ... RETURNING *, extract table from INTO/UPDATE/FROM
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
update_match = re.search(r"UPDATE\s+([^\s]+)\s+SET", sql, re.IGNORECASE)
|
|
3574
|
-
if update_match:
|
|
3575
|
-
table_name = update_match.group(1)
|
|
3576
|
-
else:
|
|
3577
|
-
# DELETE FROM table_name ...
|
|
3578
|
-
delete_match = re.search(r"DELETE\s+FROM\s+([^\s]+)", sql, re.IGNORECASE)
|
|
3579
|
-
if delete_match:
|
|
3580
|
-
table_name = delete_match.group(1)
|
|
3581
|
+
table_regex = (
|
|
3582
|
+
r'(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(?:(?:"?\w+"?)\s*\.\s*)*"?(\w+)"?'
|
|
3583
|
+
)
|
|
3584
|
+
table_match = re.search(table_regex, sql, re.IGNORECASE)
|
|
3585
|
+
if table_match:
|
|
3586
|
+
table_name = table_match.group(1)
|
|
3581
3587
|
else:
|
|
3582
3588
|
# SELECT * FROM table_name ...
|
|
3583
3589
|
from_match = re.search(r"FROM\s+([^\s,;()]+)", sql, re.IGNORECASE)
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
iris_pgwire/__init__.py,sha256=
|
|
1
|
+
iris_pgwire/__init__.py,sha256=XdG-aGnecQsLdu9Y3YON1Nnx8NGljGiYgAo1IC8WUaw,1097
|
|
2
2
|
iris_pgwire/backend_selector.py,sha256=e0DymV-TsGkvp2iItcxkSWSUM8Nf_Agw-LDo78itW04,9315
|
|
3
3
|
iris_pgwire/bulk_executor.py,sha256=nUp5e1ARUzRzFYmctqCS5Zh1gh09IV8H5-Sr2IbKbWg,12742
|
|
4
4
|
iris_pgwire/column_validator.py,sha256=0T2UlLzwH0mEsDkkPXqiUGz4t49o6ySBGtLRyeiGyQc,6806
|
|
@@ -7,12 +7,12 @@ iris_pgwire/constitutional.py,sha256=4rrl0DdZRzzcT8mppie93ajiZ_WI-TyBDxZ_Z_ZLGY8
|
|
|
7
7
|
iris_pgwire/copy_handler.py,sha256=uxvlOcdLiK91qASMcYDlY7rtDc2fmgDmZS9tjTE0WQw,15647
|
|
8
8
|
iris_pgwire/csv_processor.py,sha256=tUv-HOvrQ9KdlBk6MbYtOFvX5Ckz6WIWgJXOhDlky4I,8564
|
|
9
9
|
iris_pgwire/dbapi_connection_pool.py,sha256=84hsbGKscQoj3o2zmNKPDgQMftN7JK0ez_WQpx1P98o,17147
|
|
10
|
-
iris_pgwire/dbapi_executor.py,sha256=
|
|
10
|
+
iris_pgwire/dbapi_executor.py,sha256=y1gqHD_UJno8ryrVEv6jUJsT9IsUeiUyEy7V7zlaXEs,40917
|
|
11
11
|
iris_pgwire/debug_tracer.py,sha256=9w6Bve8cnzFPPa69ux3OcR5DMLtjI_NdEFw8TvuwlPM,11112
|
|
12
12
|
iris_pgwire/health_checker.py,sha256=5U7HHKmpzn_G9gcExbRTlJZdD47v4a2FqACVNMpURys,7094
|
|
13
13
|
iris_pgwire/integratedml.py,sha256=TjGX3Z8eXzszqABFrfcVnKp-vyzg5yOBQN8KkTws_o0,16031
|
|
14
14
|
iris_pgwire/iris_constructs.py,sha256=HJ3wNbZyxdlL_ZOganH0830AshPTbctxNTkt6GishfA,31739
|
|
15
|
-
iris_pgwire/iris_executor.py,sha256=
|
|
15
|
+
iris_pgwire/iris_executor.py,sha256=t91AYl3OOITbJVFgAJV_Wju8LzF3GeBtxFjMfQL84nA,188932
|
|
16
16
|
iris_pgwire/iris_log_handler.py,sha256=FrxCyXDwni7aVsmyAx8zlGGbPTAuRSH3j8FNY5OQdLU,3056
|
|
17
17
|
iris_pgwire/iris_user_management.py,sha256=eGN3CCJML-UVfgR4W0mvNHYQJmgMY5NZXT_SAljONdM,23780
|
|
18
18
|
iris_pgwire/observability.py,sha256=FElftKlwf4JNL3RAR1IiK7aQv2I1w_SCWk-uc6DggIk,3308
|
|
@@ -96,8 +96,8 @@ iris_pgwire/sql_translator/mappings/document_filters.py,sha256=gjkwxspizx9ObHUHJ
|
|
|
96
96
|
iris_pgwire/sql_translator/mappings/functions.py,sha256=9-lHfvGKbHlTMOdKtu0k6wH-mUth3alGZYQ5WXexWQI,19982
|
|
97
97
|
iris_pgwire/testing/__init__.py,sha256=_HBL11torW_QjZh2X3DusLlkmFp9YNb0fYoo_0_Jo3U,46
|
|
98
98
|
iris_pgwire/testing/base_fixture_builder.py,sha256=gVMGEXWjPkMPCpmvFzX9Yc5ZGLKtY8c1yj0YruHcwd8,11593
|
|
99
|
-
iris_pgwire-1.2.
|
|
100
|
-
iris_pgwire-1.2.
|
|
101
|
-
iris_pgwire-1.2.
|
|
102
|
-
iris_pgwire-1.2.
|
|
103
|
-
iris_pgwire-1.2.
|
|
99
|
+
iris_pgwire-1.2.34.dist-info/METADATA,sha256=FWOonpOtIoSbwOaLFND6rewkb1AmK45DZ1oZGSDJ0Gc,14660
|
|
100
|
+
iris_pgwire-1.2.34.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
101
|
+
iris_pgwire-1.2.34.dist-info/entry_points.txt,sha256=pUOj368R5kSiRFbk4sqKUymPgBRNK3MZ9oYWAX5WZ3k,56
|
|
102
|
+
iris_pgwire-1.2.34.dist-info/licenses/LICENSE,sha256=7C-kc5Ll8ZAWzCD882nr7ptzHdwhc-LXm-wpQ3AxRaU,1099
|
|
103
|
+
iris_pgwire-1.2.34.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|