iris-vector-rag 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. adapters/rag_templates_bridge.py +578 -0
  2. common/__init__.py +1 -0
  3. common/batch_utils.py +294 -0
  4. common/config.py +58 -0
  5. common/connection_manager.py +242 -0
  6. common/connection_pool.py +445 -0
  7. common/database.py +141 -0
  8. common/database_schema_manager.py +289 -0
  9. common/db_init_with_indexes.py +181 -0
  10. common/db_vector_utils.py +181 -0
  11. common/environment_manager.py +220 -0
  12. common/huggingface_utils.py +206 -0
  13. common/iris_client.py +92 -0
  14. common/iris_connection_manager.py +411 -0
  15. common/iris_connection_pool.py +360 -0
  16. common/iris_connector.py +71 -0
  17. common/iris_dbapi_connector.py +323 -0
  18. common/iris_index_utils.py +195 -0
  19. common/iris_sql_utils.py +704 -0
  20. common/iris_stream_reader.py +166 -0
  21. common/llm_cache_config.py +308 -0
  22. common/llm_cache_iris.py +671 -0
  23. common/llm_cache_manager.py +911 -0
  24. common/utils.py +559 -0
  25. common/vector_sql_utils.py +584 -0
  26. evaluation_framework/__init__.py +66 -0
  27. evaluation_framework/biomedical_question_generator.py +826 -0
  28. evaluation_framework/comparative_analysis_system.py +1173 -0
  29. evaluation_framework/empirical_reporting.py +1298 -0
  30. evaluation_framework/evaluation_orchestrator.py +1123 -0
  31. evaluation_framework/execute_production_evaluation.py +382 -0
  32. evaluation_framework/pmc_data_pipeline.py +1332 -0
  33. evaluation_framework/production_evaluation_config.py +353 -0
  34. evaluation_framework/ragas_metrics_framework.py +1013 -0
  35. evaluation_framework/real_production_evaluation.py +544 -0
  36. evaluation_framework/run_10k_evaluation.py +175 -0
  37. evaluation_framework/simple_test.py +364 -0
  38. evaluation_framework/statistical_evaluation_methodology.py +1016 -0
  39. evaluation_framework/test_framework.py +281 -0
  40. evaluation_framework/test_iris_connectivity.py +244 -0
  41. evaluation_framework/test_small_scale_evaluation.py +432 -0
  42. evaluation_framework/true_e2e_evaluation.py +639 -0
  43. evaluation_framework/visualization_engine.py +585 -0
  44. iris_rag/__init__.py +284 -0
  45. iris_rag/config/default_config.yaml +295 -0
  46. iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
  47. iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
  48. iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
  49. iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
  50. iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Database Schema Manager for RAG Templates
4
+ Provides centralized, config-driven table and column name resolution.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import yaml
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class TableConfig:
19
+ """Configuration for a database table."""
20
+
21
+ name: str
22
+ alias: Optional[str] = None
23
+ description: str = ""
24
+ columns: Dict[str, str] = field(default_factory=dict)
25
+ indexes: List[Dict[str, Any]] = field(default_factory=list)
26
+
27
+
28
+ @dataclass
29
+ class SchemaConfig:
30
+ """Complete database schema configuration."""
31
+
32
+ name: str
33
+ description: str = ""
34
+ tables: Dict[str, TableConfig] = field(default_factory=dict)
35
+ column_mappings: Dict[str, Any] = field(default_factory=dict)
36
+ data_types: Dict[str, str] = field(default_factory=dict)
37
+
38
+
39
+ class DatabaseSchemaManager:
40
+ """
41
+ Centralized manager for database schema configuration.
42
+ Eliminates hardcoded table/column names throughout the codebase.
43
+ """
44
+
45
+ def __init__(self, config_path: Optional[str] = None):
46
+ """Initialize with schema configuration."""
47
+ if config_path is None:
48
+ # Default to config/database_schema.yaml in project root
49
+ project_root = Path(__file__).parent.parent
50
+ config_path = project_root / "config" / "database_schema.yaml"
51
+
52
+ self.config_path = Path(config_path)
53
+ self.schema_config = self._load_schema_config()
54
+
55
+ # Cache for performance
56
+ self._table_cache = {}
57
+ self._column_cache = {}
58
+
59
+ logger.info(f"✅ Database schema manager initialized from {config_path}")
60
+
61
+ def _load_schema_config(self) -> SchemaConfig:
62
+ """Load schema configuration from YAML file."""
63
+ try:
64
+ with open(self.config_path, "r") as f:
65
+ config_data = yaml.safe_load(f)
66
+
67
+ # Parse schema info
68
+ schema_info = config_data.get("schema", {})
69
+ schema_name = schema_info.get("name", "RAG")
70
+ schema_desc = schema_info.get("description", "")
71
+
72
+ # Parse tables
73
+ tables = {}
74
+ tables_data = config_data.get("tables", {})
75
+ for table_key, table_data in tables_data.items():
76
+ tables[table_key] = TableConfig(
77
+ name=table_data.get("name", table_key),
78
+ alias=table_data.get("alias"),
79
+ description=table_data.get("description", ""),
80
+ columns=table_data.get("columns", {}),
81
+ indexes=table_data.get("indexes", []),
82
+ )
83
+
84
+ return SchemaConfig(
85
+ name=schema_name,
86
+ description=schema_desc,
87
+ tables=tables,
88
+ column_mappings=config_data.get("column_mappings", {}),
89
+ data_types=config_data.get("data_types", {}),
90
+ )
91
+
92
+ except Exception as e:
93
+ logger.error(f"Failed to load schema config from {self.config_path}: {e}")
94
+ raise
95
+
96
+ def get_schema_name(self) -> str:
97
+ """Get the schema name (e.g., 'RAG')."""
98
+ return self.schema_config.name
99
+
100
+ def get_table_name(self, table_key: str, fully_qualified: bool = True) -> str:
101
+ """
102
+ Get the actual table name for a logical table key.
103
+
104
+ Args:
105
+ table_key: Logical name (e.g., 'source_documents')
106
+ fully_qualified: Whether to include schema prefix (e.g., 'RAG.SourceDocuments')
107
+
108
+ Returns:
109
+ Actual table name
110
+ """
111
+ if table_key in self._table_cache:
112
+ table_name = self._table_cache[table_key]
113
+ else:
114
+ if table_key not in self.schema_config.tables:
115
+ raise ValueError(f"Unknown table key: {table_key}")
116
+
117
+ table_config = self.schema_config.tables[table_key]
118
+ table_name = table_config.name
119
+ self._table_cache[table_key] = table_name
120
+
121
+ if fully_qualified:
122
+ return f"{self.schema_config.name}.{table_name}"
123
+ return table_name
124
+
125
+ def get_column_name(self, table_key: str, column_key: str) -> str:
126
+ """
127
+ Get the actual column name for a logical column key.
128
+
129
+ Args:
130
+ table_key: Logical table name
131
+ column_key: Logical column name
132
+
133
+ Returns:
134
+ Actual column name
135
+ """
136
+ cache_key = f"{table_key}.{column_key}"
137
+ if cache_key in self._column_cache:
138
+ return self._column_cache[cache_key]
139
+
140
+ if table_key not in self.schema_config.tables:
141
+ raise ValueError(f"Unknown table key: {table_key}")
142
+
143
+ table_config = self.schema_config.tables[table_key]
144
+ if column_key not in table_config.columns:
145
+ raise ValueError(
146
+ f"Unknown column key '{column_key}' for table '{table_key}'"
147
+ )
148
+
149
+ column_name = table_config.columns[column_key]
150
+ self._column_cache[cache_key] = column_name
151
+ return column_name
152
+
153
+ def get_qualified_column(self, table_key: str, column_key: str) -> str:
154
+ """Get fully qualified column name (table.column)."""
155
+ table_name = self.get_table_name(table_key, fully_qualified=False)
156
+ column_name = self.get_column_name(table_key, column_key)
157
+ return f"{table_name}.{column_name}"
158
+
159
+ def standardize_column_name(self, column_name: str) -> str:
160
+ """
161
+ Standardize a column name using the mappings.
162
+
163
+ Args:
164
+ column_name: Original column name
165
+
166
+ Returns:
167
+ Standardized column name
168
+ """
169
+ mappings = self.schema_config.column_mappings
170
+
171
+ # Check each mapping category
172
+ for mapping_key, mapping_data in mappings.items():
173
+ if isinstance(mapping_data, dict) and "standard" in mapping_data:
174
+ variants = mapping_data.get("variants", [])
175
+ if column_name in variants:
176
+ return mapping_data["standard"]
177
+
178
+ # Return original if no mapping found
179
+ return column_name
180
+
181
+ def get_data_type(self, type_key: str) -> str:
182
+ """Get data type definition."""
183
+ return self.schema_config.data_types.get(type_key, "VARCHAR(255)")
184
+
185
+ def build_create_table_sql(self, table_key: str) -> str:
186
+ """Generate CREATE TABLE SQL for a table."""
187
+ if table_key not in self.schema_config.tables:
188
+ raise ValueError(f"Unknown table key: {table_key}")
189
+
190
+ table_config = self.schema_config.tables[table_key]
191
+ table_name = self.get_table_name(table_key, fully_qualified=True)
192
+
193
+ # Build column definitions
194
+ column_defs = []
195
+ for col_key, col_name in table_config.columns.items():
196
+ data_type = self.get_data_type(col_key)
197
+ column_defs.append(f" {col_name} {data_type}")
198
+
199
+ sql = f"CREATE TABLE IF NOT EXISTS {table_name} (\n"
200
+ sql += ",\n".join(column_defs)
201
+ sql += "\n);"
202
+
203
+ return sql
204
+
205
+ def get_all_tables(self) -> Dict[str, str]:
206
+ """Get mapping of all table keys to actual names."""
207
+ return {
208
+ key: self.get_table_name(key, fully_qualified=True)
209
+ for key in self.schema_config.tables.keys()
210
+ }
211
+
212
+ def validate_table_exists(self, table_key: str) -> bool:
213
+ """Check if a table configuration exists."""
214
+ return table_key in self.schema_config.tables
215
+
216
+ def get_table_info(self, table_key: str) -> Dict[str, Any]:
217
+ """Get complete information about a table."""
218
+ if table_key not in self.schema_config.tables:
219
+ raise ValueError(f"Unknown table key: {table_key}")
220
+
221
+ table_config = self.schema_config.tables[table_key]
222
+ return {
223
+ "key": table_key,
224
+ "name": table_config.name,
225
+ "alias": table_config.alias,
226
+ "description": table_config.description,
227
+ "fully_qualified_name": self.get_table_name(
228
+ table_key, fully_qualified=True
229
+ ),
230
+ "columns": table_config.columns,
231
+ "indexes": table_config.indexes,
232
+ }
233
+
234
+
235
+ # Global schema manager instance
236
+ _schema_manager = None
237
+
238
+
239
+ def get_schema_manager() -> DatabaseSchemaManager:
240
+ """Get the global schema manager instance."""
241
+ global _schema_manager
242
+ if _schema_manager is None:
243
+ _schema_manager = DatabaseSchemaManager()
244
+ return _schema_manager
245
+
246
+
247
+ # Convenience functions for common operations
248
+ def get_table(table_key: str, fully_qualified: bool = True) -> str:
249
+ """Convenience function to get table name."""
250
+ return get_schema_manager().get_table_name(table_key, fully_qualified)
251
+
252
+
253
+ def get_column(table_key: str, column_key: str) -> str:
254
+ """Convenience function to get column name."""
255
+ return get_schema_manager().get_column_name(table_key, column_key)
256
+
257
+
258
+ def get_qualified_column(table_key: str, column_key: str) -> str:
259
+ """Convenience function to get qualified column name."""
260
+ return get_schema_manager().get_qualified_column(table_key, column_key)
261
+
262
+
263
+ # Example usage and testing
264
+ if __name__ == "__main__":
265
+ # Test the schema manager
266
+ logging.basicConfig(level=logging.INFO)
267
+
268
+ manager = DatabaseSchemaManager()
269
+
270
+ print("🔍 Testing Database Schema Manager")
271
+ print("=" * 50)
272
+
273
+ # Test table names
274
+ print("Table Names:")
275
+ for table_key in ["source_documents", "document_chunks", "document_entities"]:
276
+ name = manager.get_table_name(table_key)
277
+ print(f" {table_key} → {name}")
278
+
279
+ print("\nColumn Names:")
280
+ for table_key in ["source_documents", "document_chunks"]:
281
+ for col_key in ["id", "content"]:
282
+ try:
283
+ col_name = manager.get_column_name(table_key, col_key)
284
+ print(f" {table_key}.{col_key} → {col_name}")
285
+ except ValueError as e:
286
+ print(f" {table_key}.{col_key} → ERROR: {e}")
287
+
288
+ print("\nCreate Table SQL:")
289
+ print(manager.build_create_table_sql("source_documents"))
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Complete database initialization script with all tables and indexes.
4
+ This script creates a fresh RAG database with all optimizations included.
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Add project root to path
12
+ sys.path.insert(0, str(Path(__file__).parent.parent))
13
+
14
+ from .iris_connection_manager import get_iris_connection
15
+
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def initialize_complete_rag_database(schema: str = "RAG"):
21
+ """
22
+ Initialize complete RAG database with all tables and indexes.
23
+
24
+ Args:
25
+ schema: Database schema name (default: "RAG")
26
+ """
27
+
28
+ conn = get_iris_connection()
29
+ cursor = conn.cursor()
30
+
31
+ try:
32
+ logger.info(f"🚀 Initializing complete RAG database in schema: {schema}")
33
+
34
+ # Read and execute the complete SQL schema
35
+ sql_file_path = Path(__file__).parent / "db_init_complete.sql"
36
+
37
+ if sql_file_path.exists():
38
+ with open(sql_file_path, "r") as f:
39
+ sql_content = f.read()
40
+
41
+ # Replace RAG with the specified schema
42
+ sql_content = sql_content.replace("RAG.", f"{schema}.")
43
+
44
+ # Split by semicolons and execute each statement
45
+ statements = [
46
+ stmt.strip() for stmt in sql_content.split(";") if stmt.strip()
47
+ ]
48
+
49
+ # Separate table creation from index creation
50
+ table_statements = []
51
+ index_statements = []
52
+
53
+ for statement in statements:
54
+ if statement and not statement.startswith("--"):
55
+ if statement.upper().startswith("CREATE INDEX"):
56
+ index_statements.append(statement)
57
+ else:
58
+ table_statements.append(statement)
59
+
60
+ # Execute table creation statements first
61
+ for i, statement in enumerate(table_statements):
62
+ try:
63
+ cursor.execute(statement)
64
+ logger.debug(
65
+ f"✅ Executed table statement {i+1}/{len(table_statements)}"
66
+ )
67
+ except Exception as e:
68
+ if (
69
+ "already exists" in str(e).lower()
70
+ or "duplicate" in str(e).lower()
71
+ ):
72
+ logger.debug(f"⚠️ Table statement {i+1} - object already exists")
73
+ else:
74
+ logger.warning(f"⚠️ Table statement {i+1} failed: {e}")
75
+
76
+ # Use the index utility for index creation
77
+ if index_statements:
78
+ logger.info("Creating indexes with proper error handling...")
79
+ failed_indexes = []
80
+ for statement in index_statements:
81
+ try:
82
+ # Replace "CREATE INDEX IF NOT EXISTS" with "CREATE INDEX"
83
+ statement = statement.replace("IF NOT EXISTS", "").replace(
84
+ "if not exists", ""
85
+ )
86
+ cursor.execute(statement)
87
+ logger.debug(f"✅ Created index: {statement[:50]}...")
88
+ except Exception as e:
89
+ error_str = str(e).lower()
90
+ if any(
91
+ indicator in error_str
92
+ for indicator in [
93
+ "already exists",
94
+ "duplicate",
95
+ "index exists",
96
+ "name already used",
97
+ ]
98
+ ):
99
+ logger.debug(
100
+ f"⚠️ Index already exists (ignored): {statement[:50]}..."
101
+ )
102
+ else:
103
+ logger.warning(
104
+ f"⚠️ Index creation failed: {statement[:50]}... Error: {e}"
105
+ )
106
+ failed_indexes.append(statement)
107
+
108
+ if failed_indexes:
109
+ logger.warning(f"⚠️ {len(failed_indexes)} indexes failed to create")
110
+ else:
111
+ logger.info("✅ All indexes created successfully")
112
+
113
+ logger.info(f"✅ Schema initialization completed for {schema}")
114
+ else:
115
+ logger.error(f"❌ SQL file not found: {sql_file_path}")
116
+ return False
117
+
118
+ # Verify tables were created
119
+ logger.info("🔍 Verifying table creation...")
120
+
121
+ expected_tables = [
122
+ "SourceDocuments",
123
+ "DocumentChunks",
124
+ ]
125
+
126
+ for table in expected_tables:
127
+ try:
128
+ cursor.execute(f"SELECT COUNT(*) FROM {schema}.{table}")
129
+ count = cursor.fetchone()[0]
130
+ logger.info(f"✅ {schema}.{table}: {count:,} rows")
131
+ except Exception as e:
132
+ logger.warning(f"❌ {schema}.{table}: {e}")
133
+
134
+ logger.info("🎉 Complete RAG database initialization successful!")
135
+ return True
136
+
137
+ except Exception as e:
138
+ logger.error(f"❌ Error during database initialization: {e}")
139
+ return False
140
+ finally:
141
+ cursor.close()
142
+
143
+
144
+ def create_schema_if_not_exists(schema: str = "RAG"):
145
+ """Create schema if it doesn't exist"""
146
+ conn = get_iris_connection()
147
+ cursor = conn.cursor()
148
+
149
+ try:
150
+ cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {schema}")
151
+ logger.info(f"✅ Schema {schema} ready")
152
+ except Exception as e:
153
+ logger.warning(f"Schema creation: {e}")
154
+ finally:
155
+ cursor.close()
156
+
157
+
158
+ if __name__ == "__main__":
159
+ import argparse
160
+
161
+ parser = argparse.ArgumentParser(description="Initialize complete RAG database")
162
+ parser.add_argument(
163
+ "--schema", default="RAG", help="Database schema name (default: RAG)"
164
+ )
165
+ args = parser.parse_args()
166
+
167
+ # Create schema first
168
+ create_schema_if_not_exists(args.schema)
169
+
170
+ # Initialize database
171
+ success = initialize_complete_rag_database(args.schema)
172
+
173
+ if success:
174
+ print(
175
+ f"🎉 Database initialization completed successfully for schema: {args.schema}"
176
+ )
177
+ print("📋 All tables and indexes are ready for:")
178
+ print(" - BasicRAG, ReRanking, CRAG")
179
+ else:
180
+ print("❌ Database initialization failed")
181
+ sys.exit(1)
@@ -0,0 +1,181 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ def insert_vector(
8
+ cursor: Any,
9
+ table_name: str,
10
+ vector_column_name: str,
11
+ vector_data: List[float],
12
+ target_dimension: int,
13
+ key_columns: Dict[str, Any],
14
+ additional_data: Optional[Dict[str, Any]] = None,
15
+ ) -> bool:
16
+ """
17
+ Inserts a record with a vector embedding into a specified table.
18
+ The vector is truncated to the target_dimension and inserted using TO_VECTOR(?).
19
+
20
+ Args:
21
+ cursor: Database cursor.
22
+ table_name: Name of the table to insert into (e.g., "RAG.DocumentTokenEmbeddings").
23
+ vector_column_name: Name of the column that stores the vector.
24
+ vector_data: The raw embedding vector (list of floats).
25
+ target_dimension: The dimension to truncate/pad the vector to.
26
+ key_columns: Dictionary of primary key or identifying column names and their values.
27
+ (e.g., {"doc_id": "id1", "token_index": 0})
28
+ additional_data: Optional dictionary of other column names and their values.
29
+ (e.g., {"token_text": "example"})
30
+
31
+ Returns:
32
+ True if insertion was successful, False otherwise.
33
+ """
34
+ # Validate cursor handle
35
+ if cursor is None:
36
+ logger.error(
37
+ f"DB Vector Util: Cannot insert vector into table '{table_name}': cursor is NULL"
38
+ )
39
+ return False
40
+
41
+ if not isinstance(vector_data, list) or not all(
42
+ isinstance(x, (float, int)) for x in vector_data
43
+ ):
44
+ logger.error(
45
+ f"DB Vector Util: Invalid vector_data format for table '{table_name}'. "
46
+ f"Expected list of floats/ints. Got type: {type(vector_data)}. Skipping insertion."
47
+ )
48
+ return False
49
+
50
+ # Truncate or pad the vector to the target dimension
51
+ processed_vector = vector_data[:target_dimension]
52
+ if len(processed_vector) < target_dimension:
53
+ logger.warning(
54
+ f"DB Vector Util: Original vector length ({len(vector_data)}) for table '{table_name}', column '{vector_column_name}' "
55
+ f"is less than target dimension ({target_dimension}). Padding with zeros."
56
+ )
57
+ processed_vector.extend([0.0] * (target_dimension - len(processed_vector)))
58
+
59
+ # Format as bracketed comma-separated string for IRIS TO_VECTOR() function
60
+ embedding_str = "[" + ",".join(map(str, processed_vector)) + "]"
61
+
62
+ all_columns_dict = {}
63
+ all_columns_dict.update(key_columns)
64
+ if additional_data:
65
+ all_columns_dict.update(additional_data)
66
+
67
+ # Separate vector column from other data for SQL construction
68
+ other_column_names = [col for col in all_columns_dict.keys()]
69
+ other_column_values = [all_columns_dict[col] for col in other_column_names]
70
+
71
+ column_names_sql = ", ".join(other_column_names + [vector_column_name])
72
+
73
+ # IMPORTANT: TO_VECTOR() does NOT accept parameter markers (?, :param, etc.)
74
+ # Must embed the vector string directly in SQL
75
+ # Use FLOAT to match how test data was created (see tests/fixtures/embedding_generator.py:247)
76
+ placeholders_list = ["?" for _ in other_column_names] + [
77
+ f"TO_VECTOR('{embedding_str}', FLOAT, {target_dimension})"
78
+ ]
79
+ placeholders_sql = ", ".join(placeholders_list)
80
+
81
+ # Use MERGE for upsert functionality to handle duplicates
82
+ # Build MERGE statement for IRIS
83
+ key_conditions = " AND ".join(
84
+ [f"target.{col} = source.{col}" for col in key_columns.keys()]
85
+ )
86
+ update_assignments = ", ".join(
87
+ [
88
+ f"{col} = source.{col}"
89
+ for col in other_column_names
90
+ if col not in key_columns
91
+ ]
92
+ )
93
+
94
+ # Separate approach: try INSERT first, if it fails due to constraint, try UPDATE
95
+ sql_query = (
96
+ f"INSERT INTO {table_name} ({column_names_sql}) VALUES ({placeholders_sql})"
97
+ )
98
+ # Don't pass embedding_str as a parameter - it's embedded in SQL above
99
+ params = other_column_values
100
+
101
+ try:
102
+ logger.debug(f"DB Vector Util: Executing INSERT: {sql_query}")
103
+ logger.debug(f"DB Vector Util: Parameters: {params}")
104
+ logger.debug(
105
+ f"DB Vector Util: Embedding string length: {len(embedding_str)} chars"
106
+ )
107
+ logger.debug(f"DB Vector Util: Vector dimension: {target_dimension}")
108
+ cursor.execute(sql_query, params)
109
+ return True
110
+ except Exception as e:
111
+ # Check for connection handle issues
112
+ error_str = str(e).lower()
113
+ if "_handle is null" in error_str or "handle is null" in error_str:
114
+ logger.error(
115
+ f"DB Vector Util: Database connection handle is NULL during vector insertion: {e}"
116
+ )
117
+ return False
118
+
119
+ # Check if it's a unique constraint violation
120
+ if "UNIQUE" in str(e) or "constraint failed" in str(e):
121
+ logger.debug(
122
+ f"DB Vector Util: INSERT failed due to duplicate key, attempting UPDATE..."
123
+ )
124
+
125
+ # Build UPDATE statement
126
+ set_clauses = []
127
+ update_params = []
128
+
129
+ # Add non-key columns to SET clause
130
+ for col in other_column_names:
131
+ if col not in key_columns:
132
+ set_clauses.append(f"{col} = ?")
133
+ update_params.append(all_columns_dict[col])
134
+
135
+ # Add vector column to SET clause - TO_VECTOR doesn't accept parameters
136
+ # Use FLOAT to match how test data was created (see tests/fixtures/embedding_generator.py:247)
137
+ set_clauses.append(
138
+ f"{vector_column_name} = TO_VECTOR('{embedding_str}', FLOAT, {target_dimension})"
139
+ )
140
+ # Don't append embedding_str to params - it's embedded in SQL above
141
+
142
+ # Add key columns to WHERE clause
143
+ where_clauses = []
144
+ for col, val in key_columns.items():
145
+ where_clauses.append(f"{col} = ?")
146
+ update_params.append(val)
147
+
148
+ if set_clauses and where_clauses:
149
+ update_sql = f"UPDATE {table_name} SET {', '.join(set_clauses)} WHERE {' AND '.join(where_clauses)}"
150
+
151
+ try:
152
+ logger.debug(f"DB Vector Util: Executing UPDATE: {update_sql}")
153
+ cursor.execute(update_sql, update_params)
154
+ return True
155
+ except Exception as update_error:
156
+ # Check for connection handle issues in UPDATE
157
+ update_error_str = str(update_error).lower()
158
+ if (
159
+ "_handle is null" in update_error_str
160
+ or "handle is null" in update_error_str
161
+ ):
162
+ logger.error(
163
+ f"DB Vector Util: Database connection handle is NULL during UPDATE: {update_error}"
164
+ )
165
+ else:
166
+ logger.error(
167
+ f"DB Vector Util: UPDATE also failed: {update_error}"
168
+ )
169
+ return False
170
+ else:
171
+ logger.error(f"DB Vector Util: Could not build UPDATE statement")
172
+ return False
173
+ else:
174
+ logger.error(
175
+ f"DB Vector Util: Error inserting vector into table '{table_name}', column '{vector_column_name}': {e}"
176
+ )
177
+ logger.error(f"DB Vector Util: Key columns: {key_columns}")
178
+ logger.error(
179
+ f"DB Vector Util: Failing embedding string (first 100 chars): {embedding_str[:100] if 'embedding_str' in locals() else 'NOT_SET'}"
180
+ )
181
+ return False