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.
Files changed (127) hide show
  1. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/PKG-INFO +1 -1
  2. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/__init__.py +1 -1
  3. iris_pgwire-1.5.1/src/iris_pgwire/_column_naming.py +123 -0
  4. iris_pgwire-1.5.1/src/iris_pgwire/_noop_cursor.py +21 -0
  5. iris_pgwire-1.5.1/src/iris_pgwire/_result_processing.py +217 -0
  6. iris_pgwire-1.5.1/src/iris_pgwire/_type_mapping.py +246 -0
  7. iris_pgwire-1.5.1/src/iris_pgwire/auth/auth_selector.py +112 -0
  8. iris_pgwire-1.5.1/src/iris_pgwire/auth/gssapi_auth.py +355 -0
  9. iris_pgwire-1.5.1/src/iris_pgwire/auth/oauth_bridge.py +383 -0
  10. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/auth/scram.py +142 -172
  11. iris_pgwire-1.5.1/src/iris_pgwire/auth/wallet_credentials.py +313 -0
  12. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/backend_selector.py +1 -0
  13. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/bulk_executor.py +139 -128
  14. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/catalog_functions.py +192 -144
  15. iris_pgwire-1.5.1/src/iris_pgwire/catalog/catalog_router.py +1061 -0
  16. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_attrdef.py +13 -19
  17. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_class.py +70 -59
  18. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_index.py +34 -10
  19. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/constitutional.py +68 -51
  20. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/ddl_splitter.py +81 -23
  21. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/copy_handler.py +24 -27
  22. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/csv_processor.py +89 -55
  23. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/dbapi_connection_pool.py +105 -108
  24. iris_pgwire-1.5.1/src/iris_pgwire/dbapi_executor.py +1309 -0
  25. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/integratedml.py +51 -63
  26. iris_pgwire-1.5.1/src/iris_pgwire/iris_constructs.py +632 -0
  27. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/iris_executor.py +429 -899
  28. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/iris_user_management.py +77 -159
  29. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/migrations/executor.py +12 -0
  30. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/backend_config.py +49 -33
  31. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/protocol.py +472 -530
  32. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/server.py +43 -42
  33. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/api.py +48 -44
  34. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/cache.py +22 -19
  35. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/confidence_analyzer.py +148 -125
  36. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/config.py +53 -123
  37. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/ddl_parser.py +169 -135
  38. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/ddl_translator.py +19 -22
  39. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/debug.py +99 -86
  40. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/default_values.py +1 -1
  41. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/error_handler.py +39 -26
  42. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/identifier_normalizer.py +46 -50
  43. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/index_validator.py +1 -1
  44. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/metrics.py +28 -255
  45. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/models.py +15 -15
  46. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/normalizer.py +0 -1
  47. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/parser.py +185 -147
  48. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/performance_monitor.py +115 -134
  49. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/translator.py +242 -207
  50. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/type_translator.py +8 -9
  51. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/validator.py +100 -64
  52. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/vector_metrics.py +120 -79
  53. iris_pgwire-1.5.1/src/iris_pgwire/vector_optimizer.py +653 -0
  54. iris_pgwire-1.4.2/src/iris_pgwire/auth/auth_selector.py +0 -254
  55. iris_pgwire-1.4.2/src/iris_pgwire/auth/gssapi_auth.py +0 -502
  56. iris_pgwire-1.4.2/src/iris_pgwire/auth/oauth_bridge.py +0 -495
  57. iris_pgwire-1.4.2/src/iris_pgwire/auth/wallet_credentials.py +0 -417
  58. iris_pgwire-1.4.2/src/iris_pgwire/catalog/catalog_router.py +0 -994
  59. iris_pgwire-1.4.2/src/iris_pgwire/dbapi_executor.py +0 -1411
  60. iris_pgwire-1.4.2/src/iris_pgwire/iris_constructs.py +0 -849
  61. iris_pgwire-1.4.2/src/iris_pgwire/vector_optimizer.py +0 -912
  62. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/.gitignore +0 -0
  63. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/LICENSE +0 -0
  64. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/README.md +0 -0
  65. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/pyproject.toml +0 -0
  66. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/auth/__init__.py +0 -0
  67. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/__init__.py +0 -0
  68. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/oid_generator.py +0 -0
  69. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
  70. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
  71. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
  72. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/catalog/pg_type.py +0 -0
  73. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/column_validator.py +0 -0
  74. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/config.py +0 -0
  75. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/config_schema.py +0 -0
  76. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/__init__.py +0 -0
  77. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
  78. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/date_horolog.py +0 -0
  79. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
  80. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/json_path.py +0 -0
  81. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/conversions/vector_syntax.py +0 -0
  82. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/debug_tracer.py +0 -0
  83. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/health_checker.py +0 -0
  84. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/iris_log_handler.py +0 -0
  85. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/migrations/__init__.py +0 -0
  86. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/migrations/__main__.py +0 -0
  87. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/__init__.py +0 -0
  88. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/connection_pool_state.py +0 -0
  89. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/dbapi_connection.py +0 -0
  90. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/ipm_metadata.py +0 -0
  91. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/models/vector_query_request.py +0 -0
  92. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/observability.py +0 -0
  93. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/performance_monitor.py +0 -0
  94. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/__init__.py +0 -0
  95. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/__main__.py +0 -0
  96. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
  97. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/documentation_validator.py +0 -0
  98. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
  99. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/security_validator.py +0 -0
  100. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/quality/validator.py +0 -0
  101. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/schema_mapper.py +0 -0
  102. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/__init__.py +0 -0
  103. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
  104. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
  105. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
  106. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
  107. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
  108. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
  109. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
  110. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
  111. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
  112. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
  113. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/constructs.py +0 -0
  114. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
  115. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
  116. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
  117. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
  118. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
  119. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/refiner.py +0 -0
  120. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
  121. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
  122. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
  123. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/statement_filter.py +0 -0
  124. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
  125. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/testing/__init__.py +0 -0
  126. {iris_pgwire-1.4.2 → iris_pgwire-1.5.1}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
  127. {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.4.2
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.4.2"
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