iris-pgwire 1.5.2__tar.gz → 1.5.4__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 (120) hide show
  1. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/PKG-INFO +1 -1
  2. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/__init__.py +1 -1
  3. iris_pgwire-1.5.4/src/iris_pgwire/conversions/vector_syntax.py +156 -0
  4. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/models/vector_query_request.py +15 -9
  5. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/mappings/constructs.py +16 -4
  6. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/normalizer.py +25 -7
  7. iris_pgwire-1.5.2/src/iris_pgwire/conversions/vector_syntax.py +0 -94
  8. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/.gitignore +0 -0
  9. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/LICENSE +0 -0
  10. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/README.md +0 -0
  11. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/pyproject.toml +0 -0
  12. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/_column_naming.py +0 -0
  13. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/_noop_cursor.py +0 -0
  14. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/_result_processing.py +0 -0
  15. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/_type_mapping.py +0 -0
  16. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/auth/__init__.py +0 -0
  17. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/auth/auth_selector.py +0 -0
  18. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/auth/gssapi_auth.py +0 -0
  19. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/auth/oauth_bridge.py +0 -0
  20. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/auth/scram.py +0 -0
  21. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/auth/wallet_credentials.py +0 -0
  22. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/backend_selector.py +0 -0
  23. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/bulk_executor.py +0 -0
  24. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/__init__.py +0 -0
  25. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/catalog_functions.py +0 -0
  26. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/catalog_router.py +0 -0
  27. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/oid_generator.py +0 -0
  28. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_attrdef.py +0 -0
  29. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_attribute.py +0 -0
  30. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_class.py +0 -0
  31. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_constraint.py +0 -0
  32. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_index.py +0 -0
  33. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_namespace.py +0 -0
  34. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/catalog/pg_type.py +0 -0
  35. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/column_validator.py +0 -0
  36. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/config.py +0 -0
  37. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/config_schema.py +0 -0
  38. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/constitutional.py +0 -0
  39. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/conversions/__init__.py +0 -0
  40. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/conversions/bulk_insert.py +0 -0
  41. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/conversions/date_horolog.py +0 -0
  42. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/conversions/ddl_idempotency.py +0 -0
  43. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/conversions/ddl_splitter.py +0 -0
  44. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/conversions/json_path.py +0 -0
  45. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/copy_handler.py +0 -0
  46. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/csv_processor.py +0 -0
  47. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/dbapi_connection_pool.py +0 -0
  48. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/dbapi_executor.py +0 -0
  49. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/debug_tracer.py +0 -0
  50. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/health_checker.py +0 -0
  51. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/integratedml.py +0 -0
  52. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/iris_constructs.py +0 -0
  53. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/iris_executor.py +0 -0
  54. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/iris_log_handler.py +0 -0
  55. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/iris_user_management.py +0 -0
  56. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/migrations/__init__.py +0 -0
  57. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/migrations/__main__.py +0 -0
  58. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/migrations/executor.py +0 -0
  59. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/models/__init__.py +0 -0
  60. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/models/backend_config.py +0 -0
  61. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/models/connection_pool_state.py +0 -0
  62. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/models/dbapi_connection.py +0 -0
  63. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/models/ipm_metadata.py +0 -0
  64. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/observability.py +0 -0
  65. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/performance_monitor.py +0 -0
  66. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/protocol.py +0 -0
  67. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/__init__.py +0 -0
  68. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/__main__.py +0 -0
  69. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/code_quality_validator.py +0 -0
  70. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/documentation_validator.py +0 -0
  71. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/package_metadata_validator.py +0 -0
  72. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/security_validator.py +0 -0
  73. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/quality/validator.py +0 -0
  74. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/schema_mapper.py +0 -0
  75. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/server.py +0 -0
  76. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/__init__.py +0 -0
  77. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/alias_extractor.py +0 -0
  78. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/api.py +0 -0
  79. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/boolean_translator.py +0 -0
  80. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/cache.py +0 -0
  81. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/confidence_analyzer.py +0 -0
  82. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/config.py +0 -0
  83. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/constraint_translator.py +0 -0
  84. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/copy_parser.py +0 -0
  85. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/date_translator.py +0 -0
  86. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/ddl_parser.py +0 -0
  87. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/ddl_translator.py +0 -0
  88. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/debug.py +0 -0
  89. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/default_values.py +0 -0
  90. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/enum_registry.py +0 -0
  91. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/enum_translator.py +0 -0
  92. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/error_handler.py +0 -0
  93. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/identifier_normalizer.py +0 -0
  94. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/index_validator.py +0 -0
  95. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/interceptor.py +0 -0
  96. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/logging_config.py +0 -0
  97. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/mappings/__init__.py +0 -0
  98. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/mappings/datatypes.py +0 -0
  99. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/mappings/document_filters.py +0 -0
  100. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/mappings/functions.py +0 -0
  101. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/metadata_cache.py +0 -0
  102. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/metrics.py +0 -0
  103. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/models.py +0 -0
  104. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/parser.py +0 -0
  105. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/performance_monitor.py +0 -0
  106. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/pipeline.py +0 -0
  107. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/refiner.py +0 -0
  108. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/reserved_words.py +0 -0
  109. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/returning_plan.py +0 -0
  110. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/skipped_table_set.py +0 -0
  111. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/statement_filter.py +0 -0
  112. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/transaction_translator.py +0 -0
  113. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/translator.py +0 -0
  114. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/type_translator.py +0 -0
  115. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/sql_translator/validator.py +0 -0
  116. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/testing/__init__.py +0 -0
  117. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/testing/base_fixture_builder.py +0 -0
  118. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/type_mapping.py +0 -0
  119. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/vector_metrics.py +0 -0
  120. {iris_pgwire-1.5.2 → iris_pgwire-1.5.4}/src/iris_pgwire/vector_optimizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-pgwire
3
- Version: 1.5.2
3
+ Version: 1.5.4
4
4
  Summary: PostgreSQL Wire Protocol Server for InterSystems IRIS - Connect BI tools, Python frameworks, and PostgreSQL clients to IRIS databases
5
5
  Project-URL: Homepage, https://github.com/intersystems-community/iris-pgwire
6
6
  Project-URL: Documentation, https://github.com/intersystems-community/iris-pgwire#readme
@@ -6,7 +6,7 @@ Based on the specification from docs/iris_pgwire_plan.md and proven patterns fro
6
6
  caretdev/sqlalchemy-iris.
7
7
  """
8
8
 
9
- __version__ = "1.5.2"
9
+ __version__ = "1.5.4"
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,156 @@
1
+ """
2
+ Utilities for translating PostgreSQL vector syntax (pgvector) to IRIS vector syntax.
3
+
4
+ IRIS HNSW index syntax (confirmed against live IRIS instance):
5
+ CREATE INDEX name ON [TABLE] schema.table (col)
6
+ AS HNSW(Distance='Cosine'[, M=16, efConstruction=64])
7
+
8
+ Distance values: 'Cosine' | 'DotProduct'
9
+ M and efConstruction are optional (IRIS defaults: M=16, efConstruction=64).
10
+ Distance is required for correct query-time behaviour.
11
+ """
12
+
13
+ import re
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Literal, Optional
16
+
17
+
18
+ @dataclass
19
+ class HnswIndexSpec:
20
+ """Parsed HNSW index specification."""
21
+
22
+ index_name: str
23
+ table_name: str # may include schema, e.g. "SQLUser.MyTable"
24
+ column_name: str
25
+ distance_metric: Literal["Cosine", "DotProduct"]
26
+ m: Optional[int] = None
27
+ ef_construction: Optional[int] = None
28
+ if_not_exists: bool = False
29
+
30
+ # Ignored PostgreSQL options (logged as warnings)
31
+ ignored_options: dict[str, Any] = field(default_factory=dict)
32
+
33
+ def to_iris_sql(self) -> str:
34
+ """Convert to IRIS SQL.
35
+
36
+ IRIS syntax:
37
+ CREATE INDEX name ON table (col) AS HNSW(Distance='Cosine'[, M=N, efConstruction=N])
38
+
39
+ Distance is always emitted so IRIS uses the right index structure at
40
+ query time (VECTOR_COSINE → Cosine, VECTOR_DOT_PRODUCT → DotProduct).
41
+ """
42
+ params = [f"Distance='{self.distance_metric}'"]
43
+ if self.m is not None:
44
+ params.append(f"M={self.m}")
45
+ if self.ef_construction is not None:
46
+ params.append(f"efConstruction={self.ef_construction}")
47
+ return (
48
+ f"CREATE INDEX {self.index_name} ON {self.table_name} "
49
+ f"({self.column_name}) AS HNSW({', '.join(params)})"
50
+ )
51
+
52
+ @classmethod
53
+ def from_postgres_sql(cls, sql: str) -> Optional["HnswIndexSpec"]:
54
+ """Parse an HNSW index statement in either pgvector or hybrid form.
55
+
56
+ Form 1 — pgvector standard (USING hnsw before column list):
57
+ CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON [schema.]table
58
+ USING hnsw (col vector_cosine_ops) [WITH (m=16, ef_construction=64)]
59
+
60
+ Form 2 — hybrid / user-written (column list before USING):
61
+ CREATE INDEX name ON [schema.]table (col)
62
+ USING HNSW [WITH (M=16, efConstruction=64, Distance='COSINE')]
63
+
64
+ Returns HnswIndexSpec if matched, None if not an HNSW statement.
65
+ Raises ValueError for unsupported operators (e.g. vector_l2_ops).
66
+ """
67
+ # Form 1: USING hnsw (col ops) [WITH (...)]
68
+ form1 = re.search(
69
+ r"CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?P<ine>IF\s+NOT\s+EXISTS\s+)?"
70
+ r"(?P<name>\w+)\s+ON\s+(?P<table>[\w.]+)\s+"
71
+ r"USING\s+hnsw\s*\(\s*(?P<col>\w+)\s+(?P<op>\w+)\s*\)"
72
+ r"(?:\s+WITH\s*\((?P<with>[^)]*)\))?",
73
+ sql,
74
+ re.IGNORECASE,
75
+ )
76
+ if form1:
77
+ op = form1.group("op").lower()
78
+ if op == "vector_l2_ops":
79
+ raise ValueError(
80
+ "IRIS does not support L2/Euclidean distance for HNSW indexes. "
81
+ "Use vector_cosine_ops or vector_ip_ops."
82
+ )
83
+ if op == "vector_cosine_ops":
84
+ distance: Literal["Cosine", "DotProduct"] = "Cosine"
85
+ elif op == "vector_ip_ops":
86
+ distance = "DotProduct"
87
+ else:
88
+ raise ValueError(f"Unsupported vector operator for HNSW: {op}")
89
+ m, ef = _parse_with_options(form1.group("with") or "")
90
+ return cls(
91
+ index_name=form1.group("name"),
92
+ table_name=form1.group("table"),
93
+ column_name=form1.group("col"),
94
+ distance_metric=distance,
95
+ m=m,
96
+ ef_construction=ef,
97
+ if_not_exists=bool(form1.group("ine")),
98
+ )
99
+
100
+ # Form 2: (col) USING HNSW [WITH (...)]
101
+ form2 = re.search(
102
+ r"CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?P<ine>IF\s+NOT\s+EXISTS\s+)?"
103
+ r"(?P<name>\w+)\s+ON\s+(?P<table>[\w.]+)\s*"
104
+ r"\(\s*(?P<col>\w+)\s*\)\s+"
105
+ r"USING\s+HNSW"
106
+ r"(?:\s+WITH\s*\((?P<with>[^)]*)\))?",
107
+ sql,
108
+ re.IGNORECASE,
109
+ )
110
+ if form2:
111
+ with_str = form2.group("with") or ""
112
+ m, ef = _parse_with_options(with_str)
113
+ distance = _parse_distance_from_with(with_str)
114
+ return cls(
115
+ index_name=form2.group("name"),
116
+ table_name=form2.group("table"),
117
+ column_name=form2.group("col"),
118
+ distance_metric=distance,
119
+ m=m,
120
+ ef_construction=ef,
121
+ if_not_exists=bool(form2.group("ine")),
122
+ )
123
+
124
+ return None
125
+
126
+
127
+ def _parse_with_options(with_str: str) -> tuple[Optional[int], Optional[int]]:
128
+ """Extract M and efConstruction / ef_construction from a WITH clause body."""
129
+ m: Optional[int] = None
130
+ ef: Optional[int] = None
131
+ for part in with_str.split(","):
132
+ kv = re.match(r"\s*(\w+)\s*=\s*(\d+)", part.strip())
133
+ if not kv:
134
+ continue
135
+ key, val = kv.group(1).lower(), int(kv.group(2))
136
+ if key == "m":
137
+ m = val
138
+ elif key in ("efconstruction", "ef_construction"):
139
+ ef = val
140
+ return m, ef
141
+
142
+
143
+ def _parse_distance_from_with(with_str: str) -> Literal["Cosine", "DotProduct"]:
144
+ """Extract Distance= value from a WITH clause body, defaulting to Cosine."""
145
+ dist = re.search(r"Distance\s*=\s*['\"]?(\w+)['\"]?", with_str, re.IGNORECASE)
146
+ if not dist:
147
+ return "Cosine"
148
+ val = dist.group(1).lower()
149
+ if val in ("dotproduct", "dot_product", "innerproduct", "inner_product"):
150
+ return "DotProduct"
151
+ return "Cosine"
152
+
153
+
154
+ def normalize_vector(vector: list[float]) -> list[float]:
155
+ """Ensure vector is in a format IRIS can handle (list of floats)."""
156
+ return [float(x) for x in vector]
@@ -22,8 +22,10 @@ class VectorQueryRequest(BaseModel):
22
22
  Vector similarity query after translation from pgvector to IRIS syntax.
23
23
 
24
24
  Lifecycle:
25
- 1. Client sends pgvector query: SELECT * FROM docs ORDER BY embedding <-> '[0.1,0.2]' LIMIT 5
26
- 2. PGWire translates to IRIS: SELECT TOP 5 * FROM docs ORDER BY VECTOR_COSINE(embedding, TO_VECTOR('[0.1,0.2]', 'DECIMAL'))
25
+ 1. Client sends pgvector query: SELECT * FROM docs ORDER BY embedding <=> '[0.1,0.2]' LIMIT 5
26
+ 2. PGWire translates to IRIS: SELECT TOP 5 * FROM docs ORDER BY (1 - VECTOR_COSINE(embedding, TO_VECTOR('[0.1,0.2]', DOUBLE)))
27
+ Note: <=> is cosine *distance* (0=identical); IRIS VECTOR_COSINE is cosine *similarity* (1=identical).
28
+ The (1 - ...) wrapper converts similarity → distance so ORDER BY ASC returns most-similar rows first.
27
29
  3. VectorQueryRequest captures both original and translated SQL + telemetry
28
30
  4. DBAPI executor uses translated_sql for execution
29
31
  5. Response includes translation_time_ms for SLA validation
@@ -98,15 +100,19 @@ class VectorQueryRequest(BaseModel):
98
100
 
99
101
  def operator_to_iris_function(self) -> str:
100
102
  """
101
- Map pgvector operator to IRIS vector function.
103
+ Map pgvector operator to IRIS vector function name (for telemetry only).
104
+
105
+ Note: <=> maps to VECTOR_COSINE but the actual SQL rewrite wraps it as
106
+ (1 - VECTOR_COSINE(...)) to convert similarity → distance. This method
107
+ returns only the base function name for display/telemetry purposes.
102
108
 
103
109
  Returns:
104
110
  IRIS function name (VECTOR_L2, VECTOR_COSINE, VECTOR_DOT_PRODUCT)
105
111
  """
106
112
  operator_map = {
107
- "<->": "VECTOR_L2", # L2 distance (Euclidean)
108
- "<=>": "VECTOR_COSINE", # Cosine distance
109
- "<#>": "VECTOR_DOT_PRODUCT", # Inner product (max)
113
+ "<->": "VECTOR_L2", # L2 distance (Euclidean) — unsupported, raises error
114
+ "<=>": "VECTOR_COSINE", # Cosine distance → emitted as (1 - VECTOR_COSINE(...))
115
+ "<#>": "VECTOR_DOT_PRODUCT", # Inner product (max) → emitted as (-VECTOR_DOT_PRODUCT(...))
110
116
  }
111
117
  return operator_map[self.vector_operator]
112
118
 
@@ -145,9 +151,9 @@ class VectorQueryRequest(BaseModel):
145
151
  json_schema_extra = {
146
152
  "example": {
147
153
  "request_id": "req-f47ac10b-58cc-4372-a567-0e02b2c3d479",
148
- "original_sql": "SELECT * FROM documents ORDER BY embedding <-> '[0.1,0.2,0.3]' LIMIT 5",
149
- "translated_sql": "SELECT TOP 5 * FROM documents ORDER BY VECTOR_COSINE(embedding, TO_VECTOR('[0.1,0.2,0.3]', 'DECIMAL'))",
150
- "vector_operator": "<->",
154
+ "original_sql": "SELECT * FROM documents ORDER BY embedding <=> '[0.1,0.2,0.3]' LIMIT 5",
155
+ "translated_sql": "SELECT TOP 5 * FROM documents ORDER BY (1 - VECTOR_COSINE(embedding, TO_VECTOR('[0.1,0.2,0.3]', DOUBLE)))",
156
+ "vector_operator": "<=>",
151
157
  "vector_column": "embedding",
152
158
  "query_vector": [0.1, 0.2, 0.3],
153
159
  "vector_dimensions": 3,
@@ -253,15 +253,27 @@ class IRISSQLConstructRegistry:
253
253
  notes="Strip IF NOT EXISTS from CREATE INDEX for IRIS compatibility",
254
254
  )
255
255
 
256
- # HNSW Index Support (PostgreSQL USING hnsw -> IRIS AS HNSW)
256
+ # HNSW Index — Form 1: pgvector USING hnsw (col ops) [WITH (...)]
257
+ # Handles schema-qualified table names (schema.table)
257
258
  self.add_construct(
258
259
  name="HNSW_INDEX",
259
- pattern=r"CREATE\s+INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(\w+)\s+USING\s+hnsw\s*\([^)]+\)",
260
- replacement=None, # Handled by post_process
260
+ pattern=r"CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?\w+\s+ON\s+[\w.]+\s+USING\s+hnsw\s*\([^)]+\)(?:\s+WITH\s*\([^)]*\))?",
261
+ replacement=None,
261
262
  post_process=self._translate_hnsw_index,
262
263
  confidence=0.95,
263
264
  construct_type=ConstructType.SYNTAX,
264
- notes="Translate PostgreSQL HNSW index to IRIS syntax",
265
+ notes="Translate pgvector USING hnsw (col ops) to IRIS AS HNSW(Distance=...)",
266
+ )
267
+ # HNSW Index — Form 2: col-first (col) USING HNSW [WITH (...)]
268
+ # e.g. CREATE INDEX i ON t (emb) USING HNSW WITH (M=16, Distance='COSINE')
269
+ self.add_construct(
270
+ name="HNSW_INDEX_COL_FIRST",
271
+ pattern=r"CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?\w+\s+ON\s+[\w.]+\s*\([^)]+\)\s+USING\s+HNSW(?:\s+WITH\s*\([^)]*\))?",
272
+ replacement=None,
273
+ post_process=self._translate_hnsw_index,
274
+ confidence=0.95,
275
+ construct_type=ConstructType.SYNTAX,
276
+ notes="Translate col-first (col) USING HNSW WITH (...) to IRIS AS HNSW(Distance=...)",
265
277
  )
266
278
 
267
279
  def _translate_hnsw_index(self, match: re.Match, full_sql: str) -> str:
@@ -330,20 +330,38 @@ class SQLTranslator:
330
330
  return result, count
331
331
 
332
332
  def _translate_vector_functions(self, sql: str) -> tuple[str, int]:
333
- """Translate pgvector function names to IRIS equivalents"""
333
+ """Translate pgvector function names to IRIS equivalents.
334
+
335
+ cosine_distance / vector_cosine_distance are pgvector distance functions
336
+ (0.0 = identical). IRIS VECTOR_COSINE is a *similarity* function (1.0 =
337
+ identical), so we wrap the call as (1 - VECTOR_COSINE(...)) to preserve
338
+ correct distance semantics.
339
+ """
334
340
  count = 0
335
- vector_functions = {
336
- "vector_cosine_distance": "VECTOR_COSINE",
337
- "cosine_distance": "VECTOR_COSINE",
341
+
342
+ # Cosine distance functions: wrap as (1 - VECTOR_COSINE(...)) so that
343
+ # ORDER BY ASC still returns most-similar rows first.
344
+ for pg_func in ("vector_cosine_distance", "cosine_distance"):
345
+ pattern = rf"\b{pg_func}\b"
346
+ if re.search(pattern, sql, re.IGNORECASE):
347
+ sql = re.sub(pattern, "VECTOR_COSINE", sql, flags=re.IGNORECASE)
348
+ # Wrap each full VECTOR_COSINE(...) call produced by this substitution.
349
+ sql = re.sub(
350
+ r"VECTOR_COSINE(\s*\([^)]*\))",
351
+ r"(1 - VECTOR_COSINE\1)",
352
+ sql,
353
+ )
354
+ count += 1
355
+
356
+ # Simple renames — no semantic adjustment needed.
357
+ simple_renames = {
338
358
  "vector_l2_distance": "VECTOR_L2_DISTANCE", # Error handled later if not supported
339
359
  "l2_distance": "VECTOR_L2_DISTANCE",
340
360
  "inner_product": "VECTOR_DOT_PRODUCT",
341
361
  "vector_dims": "VECTOR_DIM",
342
362
  "vector_norm": "VECTOR_NORM",
343
363
  }
344
-
345
- # Case-insensitive replacement
346
- for pg_func, iris_func in vector_functions.items():
364
+ for pg_func, iris_func in simple_renames.items():
347
365
  pattern = rf"\b{pg_func}\b"
348
366
  if re.search(pattern, sql, re.IGNORECASE):
349
367
  sql = re.sub(pattern, iris_func, sql, flags=re.IGNORECASE)
@@ -1,94 +0,0 @@
1
- """
2
- Utilities for translating PostgreSQL vector syntax (pgvector) to IRIS vector syntax.
3
- """
4
-
5
- import re
6
- from dataclasses import dataclass, field
7
- from typing import Any, Literal, Optional
8
-
9
-
10
- @dataclass
11
- class HnswIndexSpec:
12
- """Parsed HNSW index specification."""
13
-
14
- index_name: str
15
- table_name: str
16
- column_name: str
17
- distance_metric: Literal["COSINE", "DOT_PRODUCT"]
18
- if_not_exists: bool = False
19
-
20
- # Ignored PostgreSQL options (logged as warnings)
21
- ignored_options: dict[str, Any] = field(default_factory=dict)
22
-
23
- def to_iris_sql(self) -> str:
24
- """
25
- Convert to IRIS SQL index creation syntax.
26
-
27
- Returns:
28
- IRIS SQL string (e.g., "CREATE INDEX idx ON table (col) AS HNSW")
29
- """
30
- # Note: IRIS doesn't specify distance metric in the CREATE INDEX statement
31
- # It's inferred from usage, but we validate support here.
32
- # Actually, IRIS HNSW index syntax is simply:
33
- # CREATE INDEX idxname ON table (col) AS HNSW
34
- return f"CREATE INDEX {self.index_name} ON {self.table_name} ({self.column_name}) AS HNSW"
35
-
36
- @classmethod
37
- def from_postgres_sql(cls, sql: str) -> Optional["HnswIndexSpec"]:
38
- """
39
- Parse a PostgreSQL HNSW index creation statement.
40
-
41
- Example: CREATE INDEX IF NOT EXISTS idx ON documents USING hnsw (embedding vector_cosine_ops)
42
-
43
- Args:
44
- sql: PostgreSQL CREATE INDEX statement
45
-
46
- Returns:
47
- HnswIndexSpec if matched, else None
48
- """
49
- # Regex to capture parts
50
- pattern = (
51
- r"CREATE\s+INDEX\s+(?P<if_not_exists>IF\s+NOT\s+EXISTS\s+)?(?P<index_name>\w+)\s+ON\s+"
52
- r"(?P<table_name>\w+)\s+USING\s+hnsw\s*\((?P<column_name>\w+)\s+(?P<operator>\w+)\)"
53
- )
54
-
55
- match = re.search(pattern, sql, re.IGNORECASE)
56
- if not match:
57
- return None
58
-
59
- operator = match.group("operator").lower()
60
-
61
- if operator == "vector_l2_ops":
62
- raise ValueError(
63
- "IRIS does not support L2/Euclidean distance for HNSW indexes. "
64
- "Use vector_cosine_ops or vector_ip_ops."
65
- )
66
-
67
- distance_metric: Literal["COSINE", "DOT_PRODUCT"]
68
- if operator == "vector_cosine_ops":
69
- distance_metric = "COSINE"
70
- elif operator == "vector_ip_ops":
71
- distance_metric = "DOT_PRODUCT"
72
- else:
73
- raise ValueError(f"Unsupported vector operator for HNSW: {operator}")
74
-
75
- return cls(
76
- index_name=match.group("index_name"),
77
- table_name=match.group("table_name"),
78
- column_name=match.group("column_name"),
79
- distance_metric=distance_metric,
80
- if_not_exists=bool(match.group("if_not_exists")),
81
- )
82
-
83
-
84
- def normalize_vector(vector: list[float]) -> list[float]:
85
- """
86
- Ensure vector is in a format IRIS can handle (list of floats).
87
-
88
- Args:
89
- vector: List of floats
90
-
91
- Returns:
92
- Normalized vector
93
- """
94
- return [float(x) for x in vector]
File without changes
File without changes
File without changes
File without changes