iris-pgwire 1.4.2__tar.gz → 1.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/PKG-INFO +1 -1
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/__init__.py +1 -1
- iris_pgwire-1.5.1/src/iris_pgwire/_column_naming.py +123 -0
- iris_pgwire-1.5.1/src/iris_pgwire/_noop_cursor.py +21 -0
- iris_pgwire-1.5.1/src/iris_pgwire/_result_processing.py +217 -0
- iris_pgwire-1.5.1/src/iris_pgwire/_type_mapping.py +246 -0
- iris_pgwire-1.5.1/src/iris_pgwire/auth/auth_selector.py +112 -0
- iris_pgwire-1.5.1/src/iris_pgwire/auth/gssapi_auth.py +355 -0
- iris_pgwire-1.5.1/src/iris_pgwire/auth/oauth_bridge.py +383 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/auth/scram.py +142 -172
- iris_pgwire-1.5.1/src/iris_pgwire/auth/wallet_credentials.py +313 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/backend_selector.py +1 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/bulk_executor.py +139 -128
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/catalog_functions.py +192 -144
- iris_pgwire-1.5.1/src/iris_pgwire/catalog/catalog_router.py +1061 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_attrdef.py +13 -19
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_class.py +70 -59
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_index.py +34 -10
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/constitutional.py +68 -51
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/ddl_splitter.py +81 -23
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/copy_handler.py +24 -27
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/csv_processor.py +89 -55
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/dbapi_connection_pool.py +105 -108
- iris_pgwire-1.5.1/src/iris_pgwire/dbapi_executor.py +1309 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/integratedml.py +51 -63
- iris_pgwire-1.5.1/src/iris_pgwire/iris_constructs.py +632 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/iris_executor.py +429 -899
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/iris_user_management.py +77 -159
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/migrations/executor.py +12 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/backend_config.py +49 -33
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/protocol.py +472 -530
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/server.py +43 -42
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/api.py +48 -44
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/cache.py +22 -19
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/confidence_analyzer.py +148 -125
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/config.py +53 -123
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/ddl_parser.py +169 -135
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/ddl_translator.py +19 -22
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/debug.py +99 -86
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/default_values.py +1 -1
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/error_handler.py +39 -26
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/identifier_normalizer.py +46 -50
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/index_validator.py +1 -1
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/metrics.py +28 -255
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/models.py +15 -15
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/normalizer.py +0 -1
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/parser.py +185 -147
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/performance_monitor.py +115 -134
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/translator.py +242 -207
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/type_translator.py +8 -9
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/validator.py +100 -64
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/vector_metrics.py +120 -79
- iris_pgwire-1.5.1/src/iris_pgwire/vector_optimizer.py +653 -0
- iris_pgwire-1.4.2/src/iris_pgwire/auth/auth_selector.py +0 -254
- iris_pgwire-1.4.2/src/iris_pgwire/auth/gssapi_auth.py +0 -502
- iris_pgwire-1.4.2/src/iris_pgwire/auth/oauth_bridge.py +0 -495
- iris_pgwire-1.4.2/src/iris_pgwire/auth/wallet_credentials.py +0 -417
- iris_pgwire-1.4.2/src/iris_pgwire/catalog/catalog_router.py +0 -994
- iris_pgwire-1.4.2/src/iris_pgwire/dbapi_executor.py +0 -1411
- iris_pgwire-1.4.2/src/iris_pgwire/iris_constructs.py +0 -849
- iris_pgwire-1.4.2/src/iris_pgwire/vector_optimizer.py +0 -912
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/.gitignore +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/LICENSE +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/README.md +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/pyproject.toml +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/auth/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/oid_generator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_type.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/column_validator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/config.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/config_schema.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/date_horolog.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/json_path.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/debug_tracer.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/health_checker.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/iris_log_handler.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/migrations/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/migrations/__main__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/connection_pool_state.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/dbapi_connection.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/ipm_metadata.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/vector_query_request.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/observability.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/performance_monitor.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/__main__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/documentation_validator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/security_validator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/validator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/schema_mapper.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/refiner.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/statement_filter.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/testing/__init__.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
- {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/type_mapping.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iris-pgwire
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: PostgreSQL Wire Protocol Server for InterSystems IRIS - Connect BI tools, Python frameworks, and PostgreSQL clients to IRIS databases
|
|
5
5
|
Project-URL: Homepage, https://github.com/intersystems-community/iris-pgwire
|
|
6
6
|
Project-URL: Documentation, https://github.com/intersystems-community/iris-pgwire#readme
|
|
@@ -6,7 +6,7 @@ Based on the specification from docs/iris_pgwire_plan.md and proven patterns fro
|
|
|
6
6
|
caretdev/sqlalchemy-iris.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "1.
|
|
9
|
+
__version__ = "1.5.1"
|
|
10
10
|
__author__ = "Thomas Dyar <thomas.dyar@intersystems.com>"
|
|
11
11
|
|
|
12
12
|
# Don't import server/protocol in __init__ to avoid sys.modules conflicts
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Column name normalization from IRIS conventions to PostgreSQL conventions.
|
|
2
|
+
|
|
3
|
+
IRIS generates generic names like HostVar_1, Expression_1, Aggregate_1
|
|
4
|
+
when no explicit alias is provided. PostgreSQL uses different conventions
|
|
5
|
+
(e.g., ?column? for unnamed expressions). This module handles the mapping.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger()
|
|
15
|
+
|
|
16
|
+
# IRIS type names → PostgreSQL short type names
|
|
17
|
+
_POSTGRES_TYPE_MAP: dict[str, str] = {
|
|
18
|
+
"integer": "int4",
|
|
19
|
+
"bigint": "int8",
|
|
20
|
+
"smallint": "int2",
|
|
21
|
+
"real": "float4",
|
|
22
|
+
"double": "float8",
|
|
23
|
+
"double precision": "float8",
|
|
24
|
+
"character varying": "varchar",
|
|
25
|
+
"character": "char",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Cast pattern: (sql_upper_fragment, cast_keyword, pg_type_name)
|
|
29
|
+
_CAST_PATTERNS: list[tuple[str, str, str]] = [
|
|
30
|
+
("::INT", "AS INTEGER", "int4"),
|
|
31
|
+
("::BIGINT", "AS BIGINT", "int8"),
|
|
32
|
+
("::SMALLINT", "AS SMALLINT", "int2"),
|
|
33
|
+
("::TEXT", "AS TEXT", "text"),
|
|
34
|
+
("::VARCHAR", "AS VARCHAR", "varchar"),
|
|
35
|
+
("::BOOL", "AS BIT", "bool"),
|
|
36
|
+
("::DATE", "AS DATE", "date"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
_GENERIC_COLUMN_NAMES = frozenset(f"column{suffix}" for suffix in ("", "1", "2", "3", "4", "5"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_iris_column_name(iris_name: str, sql: str, iris_type: str | int) -> str:
|
|
43
|
+
"""Normalize IRIS-generated column names to PostgreSQL-compatible names.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
iris_name: Original column name from IRIS.
|
|
47
|
+
sql: Original SQL query for context.
|
|
48
|
+
iris_type: IRIS type code for type-specific naming.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
PostgreSQL-compatible column name.
|
|
52
|
+
"""
|
|
53
|
+
normalized = iris_name.lower()
|
|
54
|
+
sql_upper = sql.upper()
|
|
55
|
+
sql_lower = sql.lower()
|
|
56
|
+
|
|
57
|
+
def _find_explicit_alias(literal_val: str) -> str | None:
|
|
58
|
+
"""Check if SQL contains 'literal_val AS alias' and return the alias."""
|
|
59
|
+
if literal_val.replace(".", "").replace("-", "").isdigit():
|
|
60
|
+
# Numeric literal: match "1 AS id", "2.5 AS score"
|
|
61
|
+
pattern = rf"\b{re.escape(literal_val)}\s+AS\s+(\w+)"
|
|
62
|
+
match = re.search(pattern, sql, re.IGNORECASE)
|
|
63
|
+
if match:
|
|
64
|
+
return match.group(1).lower()
|
|
65
|
+
else:
|
|
66
|
+
# String literal: match "'first' AS name" or '"hello" AS greeting'
|
|
67
|
+
match = re.search(
|
|
68
|
+
rf"'{re.escape(literal_val)}'\s+AS\s+(\w+)", sql, re.IGNORECASE
|
|
69
|
+
) or re.search(rf'"{re.escape(literal_val)}"\s+AS\s+(\w+)', sql, re.IGNORECASE)
|
|
70
|
+
if match:
|
|
71
|
+
return match.group(1).lower()
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# --- Numeric literal column names (e.g., '1', '42', '3.14', '-5') ---
|
|
75
|
+
try:
|
|
76
|
+
float(normalized)
|
|
77
|
+
explicit_alias = _find_explicit_alias(normalized)
|
|
78
|
+
if explicit_alias:
|
|
79
|
+
logger.debug(
|
|
80
|
+
"numeric literal with explicit alias",
|
|
81
|
+
iris_name=iris_name,
|
|
82
|
+
alias=explicit_alias,
|
|
83
|
+
)
|
|
84
|
+
return explicit_alias
|
|
85
|
+
return "?column?"
|
|
86
|
+
except ValueError:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# --- SELECT without FROM: generic names → ?column? ---
|
|
90
|
+
if "SELECT" in sql_upper and "FROM" not in sql_upper:
|
|
91
|
+
if normalized in _GENERIC_COLUMN_NAMES:
|
|
92
|
+
if f" as {normalized}" not in sql_lower and f' as "{normalized}"' not in sql_lower:
|
|
93
|
+
return "?column?"
|
|
94
|
+
|
|
95
|
+
# String literal used as column name → ?column?
|
|
96
|
+
unquoted = normalized.replace("'", "").replace('"', "").strip()
|
|
97
|
+
if f"'{unquoted}'" in sql_lower or f'"{unquoted}"' in sql_lower:
|
|
98
|
+
return "?column?"
|
|
99
|
+
|
|
100
|
+
# --- HostVar_N (unnamed literals) → ?column? ---
|
|
101
|
+
if normalized.startswith("hostvar_"):
|
|
102
|
+
return "?column?"
|
|
103
|
+
|
|
104
|
+
# --- Expression_N (casts/expressions) → type name or ?column? ---
|
|
105
|
+
if normalized.startswith("expression_"):
|
|
106
|
+
for shorthand, cast_keyword, pg_type in _CAST_PATTERNS:
|
|
107
|
+
if shorthand in sql_upper or ("CAST" in sql_upper and cast_keyword in sql_upper):
|
|
108
|
+
return pg_type
|
|
109
|
+
return "?column?"
|
|
110
|
+
|
|
111
|
+
# --- Aggregate_N (aggregate functions) → function name ---
|
|
112
|
+
if normalized.startswith("aggregate_"):
|
|
113
|
+
for func in ("COUNT", "SUM", "AVG", "MIN", "MAX"):
|
|
114
|
+
if f"{func}(" in sql_upper:
|
|
115
|
+
return func.lower()
|
|
116
|
+
return normalized
|
|
117
|
+
|
|
118
|
+
# --- PostgreSQL type name mapping (INTEGER → int4, etc.) ---
|
|
119
|
+
if normalized in _POSTGRES_TYPE_MAP:
|
|
120
|
+
return _POSTGRES_TYPE_MAP[normalized]
|
|
121
|
+
|
|
122
|
+
# --- Named columns: keep lowercased ---
|
|
123
|
+
return normalized
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""No-op cursor for skipped or idempotent DDL statements."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NoopCursor:
|
|
5
|
+
"""Cursor that returns no results. Used for skipped/comment-only/idempotent DDL."""
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.description = None
|
|
9
|
+
self.rowcount = 0
|
|
10
|
+
|
|
11
|
+
def close(self):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def fetchall(self):
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
def fetchone(self):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
def __iter__(self):
|
|
21
|
+
return iter([])
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Row/column post-processing for IRIS result sets.
|
|
3
|
+
|
|
4
|
+
Extracted from duplicated logic in iris_executor._sync_execute (embedded)
|
|
5
|
+
and iris_executor._sync_external_execute (external) to provide a single
|
|
6
|
+
implementation for date/timestamp conversion and IRIS type OID overrides.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime as dt
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
from .conversions import horolog_to_pg
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
# IRIS POSIXTIME constants (canonical source: iris_executor module-level)
|
|
21
|
+
POSIXTIME_OFFSET: int = 1152921504606846976
|
|
22
|
+
POSIXTIME_MAX: int = POSIXTIME_OFFSET + 7258118400000000 # ~2200-01-01
|
|
23
|
+
|
|
24
|
+
# PostgreSQL J2000 epoch
|
|
25
|
+
_PG_EPOCH = dt.date(2000, 1, 1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def serialize_value(value: Any, type_oid: int) -> Any:
|
|
29
|
+
"""Serialize a single cell value for PostgreSQL wire protocol compatibility.
|
|
30
|
+
|
|
31
|
+
Converts IRIS-specific types (POSIXTIME microsecond integers, datetime
|
|
32
|
+
objects, digit-string timestamps) into the PostgreSQL text wire format
|
|
33
|
+
expected by client drivers such as psycopg.
|
|
34
|
+
|
|
35
|
+
Returns the value unchanged when no conversion applies.
|
|
36
|
+
"""
|
|
37
|
+
if value is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
# OID 1114 = TIMESTAMP
|
|
41
|
+
# PostgreSQL text wire format: "YYYY-MM-DD HH:MM:SS.ffffff"
|
|
42
|
+
# (space separator, no trailing Z/timezone -- psycopg TimestampLoader requires this)
|
|
43
|
+
if type_oid == 1114:
|
|
44
|
+
if isinstance(value, int):
|
|
45
|
+
try:
|
|
46
|
+
if value >= POSIXTIME_OFFSET:
|
|
47
|
+
# IRIS POSIXTIME (microseconds since 1970-01-01)
|
|
48
|
+
unix_us = value - POSIXTIME_OFFSET
|
|
49
|
+
ts_obj = dt.datetime(1970, 1, 1) + dt.timedelta(microseconds=unix_us)
|
|
50
|
+
else:
|
|
51
|
+
# PostgreSQL legacy/IRIS microsecond integer (microseconds since 2000-01-01)
|
|
52
|
+
ts_obj = dt.datetime(2000, 1, 1) + dt.timedelta(microseconds=value)
|
|
53
|
+
return ts_obj.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
54
|
+
except Exception:
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
if isinstance(value, dt.datetime):
|
|
58
|
+
return value.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
59
|
+
|
|
60
|
+
if isinstance(value, str):
|
|
61
|
+
stripped = value.strip()
|
|
62
|
+
if stripped.isdigit():
|
|
63
|
+
# POSIXTIME encoded as digit string
|
|
64
|
+
unix_us = int(stripped) - POSIXTIME_OFFSET
|
|
65
|
+
ts_obj = dt.datetime(1970, 1, 1) + dt.timedelta(microseconds=unix_us)
|
|
66
|
+
return ts_obj.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
67
|
+
|
|
68
|
+
# Pre-decoded datetime string from IRIS driver -- parse and reformat
|
|
69
|
+
for fmt in (
|
|
70
|
+
"%Y-%m-%d %H:%M:%S.%f",
|
|
71
|
+
"%Y-%m-%d %H:%M:%S",
|
|
72
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
73
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
74
|
+
):
|
|
75
|
+
try:
|
|
76
|
+
ts_obj = dt.datetime.strptime(stripped.rstrip("Z"), fmt)
|
|
77
|
+
return ts_obj.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
78
|
+
except ValueError:
|
|
79
|
+
continue
|
|
80
|
+
return value # unrecognised format -- pass through unchanged
|
|
81
|
+
|
|
82
|
+
# OID 1082 = DATE (integer Horolog already converted elsewhere; safety passthrough)
|
|
83
|
+
if type_oid == 1082 and isinstance(value, int):
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Row / column post-processing
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def convert_rows_dates_and_timestamps(
|
|
95
|
+
rows: list[list[Any]],
|
|
96
|
+
columns: list[dict[str, Any]],
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Convert IRIS date/timestamp values in *rows* to PostgreSQL wire format.
|
|
99
|
+
|
|
100
|
+
Mutates *rows* and *columns* in-place:
|
|
101
|
+
|
|
102
|
+
1. Builds a ``column_type_oids`` lookup from *columns*.
|
|
103
|
+
2. For integer columns (OID 20/23) whose value falls within the POSIXTIME
|
|
104
|
+
range, reclassifies the column as TIMESTAMP (OID 1114) and serializes
|
|
105
|
+
the value accordingly.
|
|
106
|
+
3. Runs :func:`serialize_value` on every cell for TIMESTAMP formatting.
|
|
107
|
+
4. Converts DATE (OID 1082) ISO strings (``YYYY-MM-DD``) to PostgreSQL
|
|
108
|
+
integer days since 2000-01-01.
|
|
109
|
+
5. Converts Horolog integer dates to PostgreSQL days via
|
|
110
|
+
:func:`~iris_pgwire.conversions.horolog_to_pg`.
|
|
111
|
+
"""
|
|
112
|
+
if not rows or not columns:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
column_type_oids = [col["type_oid"] for col in columns]
|
|
116
|
+
|
|
117
|
+
for row_idx, row in enumerate(rows):
|
|
118
|
+
for col_idx, value in enumerate(row):
|
|
119
|
+
if col_idx >= len(column_type_oids):
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
type_oid = column_type_oids[col_idx]
|
|
123
|
+
|
|
124
|
+
# Detect POSIXTIME integers masquerading as INT8/INT4
|
|
125
|
+
if type_oid in (20, 23) and isinstance(value, int):
|
|
126
|
+
if POSIXTIME_OFFSET <= value <= POSIXTIME_MAX:
|
|
127
|
+
type_oid = 1114
|
|
128
|
+
if row_idx == 0:
|
|
129
|
+
columns[col_idx]["type_oid"] = 1114
|
|
130
|
+
|
|
131
|
+
# Robust serialization (TIMESTAMP, etc.)
|
|
132
|
+
rows[row_idx][col_idx] = serialize_value(rows[row_idx][col_idx], type_oid)
|
|
133
|
+
value = rows[row_idx][col_idx]
|
|
134
|
+
|
|
135
|
+
# OID 1082 = DATE
|
|
136
|
+
if type_oid == 1082 and value is not None:
|
|
137
|
+
try:
|
|
138
|
+
if isinstance(value, str):
|
|
139
|
+
# IRIS returns dates as ISO strings (YYYY-MM-DD)
|
|
140
|
+
date_obj = dt.datetime.strptime(value, "%Y-%m-%d").date()
|
|
141
|
+
pg_days = (date_obj - _PG_EPOCH).days
|
|
142
|
+
rows[row_idx][col_idx] = pg_days
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Converted date string to PostgreSQL format",
|
|
145
|
+
row=row_idx,
|
|
146
|
+
col=col_idx,
|
|
147
|
+
iris_string=value,
|
|
148
|
+
pg_days=pg_days,
|
|
149
|
+
date_obj=str(date_obj),
|
|
150
|
+
)
|
|
151
|
+
elif isinstance(value, int):
|
|
152
|
+
pg_date = horolog_to_pg(value)
|
|
153
|
+
rows[row_idx][col_idx] = pg_date
|
|
154
|
+
logger.debug(
|
|
155
|
+
"Converted Horolog date to PostgreSQL format",
|
|
156
|
+
row=row_idx,
|
|
157
|
+
col=col_idx,
|
|
158
|
+
iris_horolog=value,
|
|
159
|
+
pg_days=pg_date,
|
|
160
|
+
)
|
|
161
|
+
except Exception as date_err:
|
|
162
|
+
logger.warning(
|
|
163
|
+
"Failed to convert date value",
|
|
164
|
+
row=row_idx,
|
|
165
|
+
col=col_idx,
|
|
166
|
+
value=value,
|
|
167
|
+
value_type=type(value).__name__,
|
|
168
|
+
error=str(date_err),
|
|
169
|
+
)
|
|
170
|
+
# Keep original value if conversion fails
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# IRIS type OID overrides
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def override_iris_type_oid(iris_type: int, type_oid: int, sql_upper: str) -> int:
|
|
179
|
+
"""Apply IRIS-specific type OID corrections for PostgreSQL compatibility.
|
|
180
|
+
|
|
181
|
+
Handles two cases where IRIS returns an incorrect or sub-optimal type OID:
|
|
182
|
+
|
|
183
|
+
* **IRIS type code 2 (NUMERIC)** -- decimal literals like ``3.14`` are
|
|
184
|
+
reported as NUMERIC. For client drivers that distinguish NUMERIC from
|
|
185
|
+
FLOAT (e.g. node-postgres), we override to FLOAT8 (OID 701) unless the
|
|
186
|
+
SQL contains an explicit ``CAST … AS NUMERIC`` / ``AS DECIMAL`` / ``AS
|
|
187
|
+
INTEGER`` / ``AS INT``. An explicit ``AS INTEGER`` / ``AS INT`` cast
|
|
188
|
+
produces INT4 (OID 23).
|
|
189
|
+
|
|
190
|
+
* **CURRENT_TIMESTAMP** -- IRIS returns OID 25 (TEXT) or 1043 (VARCHAR)
|
|
191
|
+
for ``CURRENT_TIMESTAMP``. Both are overridden to 1114 (TIMESTAMP) so
|
|
192
|
+
that client drivers (Npgsql, psycopg) handle the value correctly.
|
|
193
|
+
|
|
194
|
+
Returns the (possibly corrected) *type_oid*.
|
|
195
|
+
"""
|
|
196
|
+
if iris_type == 2:
|
|
197
|
+
if "AS INTEGER" in sql_upper or "AS INT" in sql_upper:
|
|
198
|
+
logger.debug(
|
|
199
|
+
"Overriding IRIS type code 2 (NUMERIC) to INT4 for explicit INTEGER cast",
|
|
200
|
+
original_oid=type_oid,
|
|
201
|
+
)
|
|
202
|
+
return 23 # INT4
|
|
203
|
+
if "AS NUMERIC" not in sql_upper and "AS DECIMAL" not in sql_upper:
|
|
204
|
+
logger.debug(
|
|
205
|
+
"Overriding IRIS type code 2 (NUMERIC) to FLOAT8 for decimal literal",
|
|
206
|
+
original_oid=type_oid,
|
|
207
|
+
)
|
|
208
|
+
return 701 # FLOAT8
|
|
209
|
+
|
|
210
|
+
if "CURRENT_TIMESTAMP" in sql_upper and type_oid in (25, 1043):
|
|
211
|
+
logger.debug(
|
|
212
|
+
"Overriding CURRENT_TIMESTAMP type to TIMESTAMP",
|
|
213
|
+
original_oid=type_oid,
|
|
214
|
+
)
|
|
215
|
+
return 1114 # TIMESTAMP
|
|
216
|
+
|
|
217
|
+
return type_oid
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Internal type-mapping helpers for IRIS ↔ PostgreSQL OID conversion.
|
|
2
|
+
|
|
3
|
+
Pure functions with no logging or external dependencies beyond stdlib + Decimal.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import datetime as dt
|
|
9
|
+
import re
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
# IRIS POSIXTIME: microseconds since 1970-01-01 with a large offset added.
|
|
14
|
+
POSIXTIME_OFFSET = 1152921504606846976
|
|
15
|
+
POSIXTIME_MAX = POSIXTIME_OFFSET + 7258118400000000 # ~2200-01-01
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Integer type-code → OID (IRIS JDBC / ODBC codes)
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
_INT_TYPE_TO_OID: dict[int, int] = {
|
|
21
|
+
-7: 16, # BIT → bool
|
|
22
|
+
-6: 21, # TINYINT → int2
|
|
23
|
+
-5: 20, # BIGINT → int8
|
|
24
|
+
1: 1042, # CHAR → bpchar
|
|
25
|
+
2: 1700, # NUMERIC
|
|
26
|
+
3: 20, # → int8
|
|
27
|
+
4: 23, # → int4
|
|
28
|
+
5: 701, # → float8
|
|
29
|
+
8: 701, # IRIS DOUBLE → float8
|
|
30
|
+
9: 1082, # → date
|
|
31
|
+
10: 1114, # → timestamp
|
|
32
|
+
12: 1043, # → varchar
|
|
33
|
+
16: 16, # → bool
|
|
34
|
+
17: 17, # → bytea
|
|
35
|
+
# JDBC standard codes
|
|
36
|
+
91: 1082, # JDBC DATE
|
|
37
|
+
92: 1083, # JDBC TIME
|
|
38
|
+
93: 1114, # JDBC TIMESTAMP
|
|
39
|
+
# IRIS extended codes (returned for TIMESTAMP/POSIXTIME columns)
|
|
40
|
+
1091: 1082,
|
|
41
|
+
1092: 1083,
|
|
42
|
+
1093: 1114,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# String type-name → OID (union of both original mappings)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
_STR_TYPE_TO_OID: dict[str, int] = {
|
|
49
|
+
"INT": 23,
|
|
50
|
+
"INTEGER": 23,
|
|
51
|
+
"BIGINT": 20,
|
|
52
|
+
"SMALLINT": 21,
|
|
53
|
+
"VARCHAR": 1043,
|
|
54
|
+
"CHAR": 1042,
|
|
55
|
+
"TEXT": 25,
|
|
56
|
+
"DATE": 1082,
|
|
57
|
+
"TIME": 1083,
|
|
58
|
+
"TIMESTAMP": 1114,
|
|
59
|
+
"DOUBLE": 701,
|
|
60
|
+
"FLOAT": 700,
|
|
61
|
+
"NUMERIC": 1700,
|
|
62
|
+
"DECIMAL": 1700,
|
|
63
|
+
"BIT": 1560,
|
|
64
|
+
"BOOLEAN": 16,
|
|
65
|
+
"BINARY": 17,
|
|
66
|
+
"VARBINARY": 17,
|
|
67
|
+
"VECTOR": 16388,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_DEFAULT_OID = 1043 # VARCHAR
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def iris_type_to_pg_oid(iris_type: str | int) -> int:
|
|
74
|
+
"""Convert an IRIS type (integer code or string name) to a PostgreSQL OID.
|
|
75
|
+
|
|
76
|
+
Handles both JDBC/ODBC integer type codes and SQL string type names
|
|
77
|
+
such as ``"VARCHAR"`` or ``"INTEGER(10)"``. Unknown types default to
|
|
78
|
+
VARCHAR (1043).
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(iris_type, int):
|
|
81
|
+
return _INT_TYPE_TO_OID.get(iris_type, _DEFAULT_OID)
|
|
82
|
+
|
|
83
|
+
# Strip size/precision suffix: "VARCHAR(255)" → "VARCHAR"
|
|
84
|
+
normalized = str(iris_type).upper().split("(")[0].strip()
|
|
85
|
+
return _STR_TYPE_TO_OID.get(normalized, _DEFAULT_OID)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Value-based type inference
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
_INT4_MIN = -(2**31)
|
|
93
|
+
_INT4_MAX = 2**31 - 1
|
|
94
|
+
_ID_HINTS = ("id", "key")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def infer_type_from_value(value: Any, column_name: str | None = None) -> int:
|
|
98
|
+
"""Infer a PostgreSQL OID from a Python value.
|
|
99
|
+
|
|
100
|
+
Uses POSIXTIME range detection for large integers and column-name hints
|
|
101
|
+
(``id`` / ``key``) to choose BIGINT over INTEGER.
|
|
102
|
+
"""
|
|
103
|
+
if value is None:
|
|
104
|
+
return 1043 # VARCHAR — most flexible for NULL
|
|
105
|
+
|
|
106
|
+
if isinstance(value, bool):
|
|
107
|
+
return 16 # BOOL
|
|
108
|
+
|
|
109
|
+
if isinstance(value, int):
|
|
110
|
+
if POSIXTIME_OFFSET <= value <= POSIXTIME_MAX:
|
|
111
|
+
return 1114 # TIMESTAMP
|
|
112
|
+
if column_name and any(k in column_name.lower() for k in _ID_HINTS):
|
|
113
|
+
return 20 # BIGINT
|
|
114
|
+
if _INT4_MIN <= value <= _INT4_MAX:
|
|
115
|
+
return 23 # INTEGER
|
|
116
|
+
return 20 # BIGINT
|
|
117
|
+
|
|
118
|
+
if isinstance(value, float):
|
|
119
|
+
return 701 # FLOAT8
|
|
120
|
+
|
|
121
|
+
if isinstance(value, Decimal):
|
|
122
|
+
return 1700 # NUMERIC
|
|
123
|
+
|
|
124
|
+
if isinstance(value, bytes):
|
|
125
|
+
return 17 # BYTEA
|
|
126
|
+
|
|
127
|
+
if isinstance(value, dt.datetime):
|
|
128
|
+
return 1114 # TIMESTAMP
|
|
129
|
+
|
|
130
|
+
if isinstance(value, dt.date):
|
|
131
|
+
return 1082 # DATE
|
|
132
|
+
|
|
133
|
+
return 1043 # VARCHAR (default for str and everything else)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Value serialisation
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
_TIMESTAMP_PARSE_FORMATS = (
|
|
141
|
+
"%Y-%m-%d %H:%M:%S.%f",
|
|
142
|
+
"%Y-%m-%d %H:%M:%S",
|
|
143
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
144
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
_PG_TIMESTAMP_FMT = "%Y-%m-%d %H:%M:%S.%f"
|
|
148
|
+
|
|
149
|
+
_EPOCH_1970 = dt.datetime(1970, 1, 1)
|
|
150
|
+
_EPOCH_2000 = dt.datetime(2000, 1, 1)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def serialize_value(value: Any, type_oid: int) -> Any:
|
|
154
|
+
"""Serialize a Python value for the PostgreSQL wire protocol.
|
|
155
|
+
|
|
156
|
+
Converts IRIS-specific representations (e.g. POSIXTIME microsecond
|
|
157
|
+
integers) into the text format PostgreSQL clients expect.
|
|
158
|
+
"""
|
|
159
|
+
if value is None:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if type_oid == 1114: # TIMESTAMP
|
|
163
|
+
return _serialize_timestamp(value)
|
|
164
|
+
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _serialize_timestamp(value: Any) -> Any:
|
|
169
|
+
"""Convert various timestamp representations to PG text format."""
|
|
170
|
+
if isinstance(value, int):
|
|
171
|
+
try:
|
|
172
|
+
if value >= POSIXTIME_OFFSET:
|
|
173
|
+
unix_us = value - POSIXTIME_OFFSET
|
|
174
|
+
ts = _EPOCH_1970 + dt.timedelta(microseconds=unix_us)
|
|
175
|
+
else:
|
|
176
|
+
# Legacy: microseconds since 2000-01-01
|
|
177
|
+
ts = _EPOCH_2000 + dt.timedelta(microseconds=value)
|
|
178
|
+
return ts.strftime(_PG_TIMESTAMP_FMT)
|
|
179
|
+
except Exception:
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
if isinstance(value, dt.datetime):
|
|
183
|
+
return value.strftime(_PG_TIMESTAMP_FMT)
|
|
184
|
+
|
|
185
|
+
if isinstance(value, str):
|
|
186
|
+
stripped = value.strip()
|
|
187
|
+
if stripped.isdigit():
|
|
188
|
+
unix_us = int(stripped) - POSIXTIME_OFFSET
|
|
189
|
+
ts = _EPOCH_1970 + dt.timedelta(microseconds=unix_us)
|
|
190
|
+
return ts.strftime(_PG_TIMESTAMP_FMT)
|
|
191
|
+
|
|
192
|
+
# Try known datetime string formats
|
|
193
|
+
for fmt in _TIMESTAMP_PARSE_FORMATS:
|
|
194
|
+
try:
|
|
195
|
+
ts = dt.datetime.strptime(stripped.rstrip("Z"), fmt)
|
|
196
|
+
return ts.strftime(_PG_TIMESTAMP_FMT)
|
|
197
|
+
except ValueError:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
return value # unrecognised — pass through
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# SQL CAST detection
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
_CAST_TYPE_MAP: dict[str, int] = {
|
|
208
|
+
"bool": 16,
|
|
209
|
+
"boolean": 16,
|
|
210
|
+
"bit": 16, # IRIS uses BIT for boolean
|
|
211
|
+
"int": 23,
|
|
212
|
+
"integer": 23,
|
|
213
|
+
"bigint": 20,
|
|
214
|
+
"smallint": 21,
|
|
215
|
+
"text": 25,
|
|
216
|
+
"varchar": 1043,
|
|
217
|
+
"date": 1082,
|
|
218
|
+
"timestamp": 1114,
|
|
219
|
+
"float": 701,
|
|
220
|
+
"double": 701,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def detect_cast_type_oid(sql: str, column_name: str) -> int | None:
|
|
225
|
+
"""Detect a PostgreSQL OID from CAST expressions targeting *column_name*.
|
|
226
|
+
|
|
227
|
+
Recognises two patterns:
|
|
228
|
+
- ``$1::bool AS column_name`` (PostgreSQL-style cast)
|
|
229
|
+
- ``CAST(? AS BIT) AS column_name`` (SQL-standard cast)
|
|
230
|
+
|
|
231
|
+
Returns ``None`` if no matching cast is found.
|
|
232
|
+
"""
|
|
233
|
+
sql_upper = sql.upper()
|
|
234
|
+
col_escaped = re.escape(column_name.upper())
|
|
235
|
+
|
|
236
|
+
# Pattern 1: $N::type AS column
|
|
237
|
+
match = re.search(rf"\$\d+::(\w+)\s+AS\s+{col_escaped}", sql_upper)
|
|
238
|
+
if match:
|
|
239
|
+
return _CAST_TYPE_MAP.get(match.group(1).lower())
|
|
240
|
+
|
|
241
|
+
# Pattern 2: CAST(? AS type) AS column
|
|
242
|
+
match = re.search(rf"CAST\(\?\s+AS\s+(\w+)\)\s+AS\s+{col_escaped}", sql_upper)
|
|
243
|
+
if match:
|
|
244
|
+
return _CAST_TYPE_MAP.get(match.group(1).lower())
|
|
245
|
+
|
|
246
|
+
return None
|