pgvector-haystack 6.2.0__py3-none-any.whl → 6.3.0__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.
@@ -2,13 +2,13 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import re
6
5
  from datetime import datetime
7
6
  from itertools import chain
8
7
  from typing import Any, Literal
9
8
 
10
9
  from haystack.errors import FilterError
11
- from psycopg.sql import SQL, Composed
10
+ from psycopg.sql import SQL, Composable, Composed, Identifier
11
+ from psycopg.sql import Literal as SQLLiteral
12
12
  from psycopg.types.json import Jsonb
13
13
 
14
14
  # we need this mapping to cast meta values to the correct type,
@@ -21,7 +21,6 @@ PYTHON_TYPES_TO_PG_TYPES = {
21
21
  }
22
22
 
23
23
  NO_VALUE = "no_value"
24
- SAFE_META_FIELD_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
25
24
 
26
25
 
27
26
  def _validate_filters(filters: dict[str, Any] | None = None) -> None:
@@ -48,13 +47,13 @@ def _convert_filters_to_where_clause_and_params(
48
47
  else:
49
48
  query, values = _parse_logical_condition(filters)
50
49
 
51
- where_clause = SQL(f" {operator} ") + SQL(query)
50
+ where_clause = SQL(f" {operator} ") + query
52
51
  params = tuple(value for value in values if value != NO_VALUE)
53
52
 
54
53
  return where_clause, params
55
54
 
56
55
 
57
- def _parse_logical_condition(condition: dict[str, Any]) -> tuple[str, list[Any]]:
56
+ def _parse_logical_condition(condition: dict[str, Any]) -> tuple[Composed, list[Any]]:
58
57
  if "operator" not in condition:
59
58
  msg = f"'operator' key missing in {condition}"
60
59
  raise FilterError(msg)
@@ -84,17 +83,14 @@ def _parse_logical_condition(condition: dict[str, Any]) -> tuple[str, list[Any]]
84
83
  values = list(chain.from_iterable(values))
85
84
 
86
85
  if operator == "AND":
87
- sql_query = f"({' AND '.join(query_parts)})"
88
- elif operator == "OR":
89
- sql_query = f"({' OR '.join(query_parts)})"
90
- else:
91
- msg = f"Unknown logical operator '{operator}'"
92
- raise FilterError(msg)
86
+ sql_query = SQL("(") + SQL(" AND ").join(query_parts) + SQL(")")
87
+ else: # operator == "OR"
88
+ sql_query = SQL("(") + SQL(" OR ").join(query_parts) + SQL(")")
93
89
 
94
90
  return sql_query, values
95
91
 
96
92
 
97
- def _parse_comparison_condition(condition: dict[str, Any]) -> tuple[str, list[Any]]:
93
+ def _parse_comparison_condition(condition: dict[str, Any]) -> tuple[Composed, list[Any]]:
98
94
  field: str = condition["field"]
99
95
  if "operator" not in condition:
100
96
  msg = f"'operator' key missing in {condition}"
@@ -110,34 +106,37 @@ def _parse_comparison_condition(condition: dict[str, Any]) -> tuple[str, list[An
110
106
  value: Any = condition["value"]
111
107
 
112
108
  if field.startswith("meta."):
113
- field = _treat_meta_field(field, value)
109
+ sql_field: Composable = _treat_meta_field(field, value)
110
+ else:
111
+ sql_field = Identifier(field)
114
112
 
115
- field, value = COMPARISON_OPERATORS[operator](field, value)
116
- return field, [value]
113
+ sql_expr, value = COMPARISON_OPERATORS[operator](sql_field, value)
114
+ return sql_expr, [value]
117
115
 
118
116
 
119
- def _treat_meta_field(field: str, value: Any) -> str:
117
+ def _treat_meta_field(field: str, value: Any) -> Composed:
120
118
  """
121
- Internal method that modifies the field str
122
- to make the meta JSONB field queryable.
119
+ Internal method that returns a psycopg Composed object
120
+ to make the meta JSONB field queryable safely.
121
+
122
+ Uses psycopg.sql.Literal to embed the field name, preventing SQL injection
123
+ via metadata field names without requiring regex validation.
124
+
125
+ Use the ->> operator to access keys in the meta JSONB field.
123
126
 
124
127
  Examples:
125
128
  >>> _treat_meta_field(field="meta.number", value=9)
126
- "(meta->>'number')::integer"
129
+ Composed([SQL('(meta->>'), Literal('number'), SQL(')::integer')])
127
130
 
128
131
  >>> _treat_meta_field(field="meta.name", value="my_name")
129
- "meta->>'name'"
130
- """
132
+ Composed([SQL('meta->>'), Literal('name')])
131
133
 
132
- # use the ->> operator to access keys in the meta JSONB field
134
+ """
133
135
  field_name = field.split(".", 1)[-1]
134
- if not SAFE_META_FIELD_RE.match(field_name):
135
- msg = (
136
- f"Invalid metadata field name '{field_name}'. "
137
- "Only alphanumeric characters, dashes, and underscores are allowed."
138
- )
139
- raise FilterError(msg)
140
- field = f"meta->>'{field_name}'"
136
+
137
+ # Use SQLLiteral to safely embed the field name as a SQL string literal,
138
+ # preventing SQL injection via metadata field names.
139
+ composed: Composed = SQL("meta->>") + SQLLiteral(field_name)
141
140
 
142
141
  # meta fields are stored as strings in the JSONB field,
143
142
  # so we need to cast them to the correct type
@@ -146,25 +145,24 @@ def _treat_meta_field(field: str, value: Any) -> str:
146
145
  type_value = PYTHON_TYPES_TO_PG_TYPES.get(type(value[0]))
147
146
 
148
147
  if type_value:
149
- field = f"({field})::{type_value}"
148
+ composed = SQL("(") + composed + SQL(f")::{type_value}")
150
149
 
151
- return field
150
+ return composed
152
151
 
153
152
 
154
- def _equal(field: str, value: Any) -> tuple[str, Any]:
153
+ def _equal(field: Composable, value: Any) -> tuple[Composed, Any]:
155
154
  if value is None:
156
- # NO_VALUE is a placeholder that will be removed in _convert_filters_to_where_clause_and_params
157
- return f"{field} IS NULL", NO_VALUE
158
- return f"{field} = %s", value
155
+ return SQL("{} IS NULL").format(field), NO_VALUE
156
+ return SQL("{} = %s").format(field), value
159
157
 
160
158
 
161
- def _not_equal(field: str, value: Any) -> tuple[str, Any]:
159
+ def _not_equal(field: Composable, value: Any) -> tuple[Composed, Any]:
162
160
  # we use IS DISTINCT FROM to correctly handle NULL values
163
161
  # (not handled by !=)
164
- return f"{field} IS DISTINCT FROM %s", value
162
+ return SQL("{} IS DISTINCT FROM %s").format(field), value
165
163
 
166
164
 
167
- def _greater_than(field: str, value: Any) -> tuple[str, Any]:
165
+ def _greater_than(field: Composable, value: Any) -> tuple[Composed, Any]:
168
166
  if isinstance(value, str):
169
167
  try:
170
168
  datetime.fromisoformat(value)
@@ -177,11 +175,10 @@ def _greater_than(field: str, value: Any) -> tuple[str, Any]:
177
175
  if type(value) in [list, Jsonb]:
178
176
  msg = f"Filter value can't be of type {type(value)} using operators '>', '>=', '<', '<='"
179
177
  raise FilterError(msg)
178
+ return SQL("{} > %s").format(field), value
180
179
 
181
- return f"{field} > %s", value
182
180
 
183
-
184
- def _greater_than_equal(field: str, value: Any) -> tuple[str, Any]:
181
+ def _greater_than_equal(field: Composable, value: Any) -> tuple[Composed, Any]:
185
182
  if isinstance(value, str):
186
183
  try:
187
184
  datetime.fromisoformat(value)
@@ -194,11 +191,10 @@ def _greater_than_equal(field: str, value: Any) -> tuple[str, Any]:
194
191
  if type(value) in [list, Jsonb]:
195
192
  msg = f"Filter value can't be of type {type(value)} using operators '>', '>=', '<', '<='"
196
193
  raise FilterError(msg)
197
-
198
- return f"{field} >= %s", value
194
+ return SQL("{} >= %s").format(field), value
199
195
 
200
196
 
201
- def _less_than(field: str, value: Any) -> tuple[str, Any]:
197
+ def _less_than(field: Composable, value: Any) -> tuple[Composed, Any]:
202
198
  if isinstance(value, str):
203
199
  try:
204
200
  datetime.fromisoformat(value)
@@ -211,11 +207,10 @@ def _less_than(field: str, value: Any) -> tuple[str, Any]:
211
207
  if type(value) in [list, Jsonb]:
212
208
  msg = f"Filter value can't be of type {type(value)} using operators '>', '>=', '<', '<='"
213
209
  raise FilterError(msg)
210
+ return SQL("{} < %s").format(field), value
214
211
 
215
- return f"{field} < %s", value
216
212
 
217
-
218
- def _less_than_equal(field: str, value: Any) -> tuple[str, Any]:
213
+ def _less_than_equal(field: Composable, value: Any) -> tuple[Composed, Any]:
219
214
  if isinstance(value, str):
220
215
  try:
221
216
  datetime.fromisoformat(value)
@@ -228,39 +223,37 @@ def _less_than_equal(field: str, value: Any) -> tuple[str, Any]:
228
223
  if type(value) in [list, Jsonb]:
229
224
  msg = f"Filter value can't be of type {type(value)} using operators '>', '>=', '<', '<='"
230
225
  raise FilterError(msg)
231
-
232
- return f"{field} <= %s", value
226
+ return SQL("{} <= %s").format(field), value
233
227
 
234
228
 
235
- def _not_in(field: str, value: Any) -> tuple[str, list]:
229
+ def _not_in(field: Composable, value: Any) -> tuple[Composed, list]:
236
230
  if not isinstance(value, list):
237
231
  msg = f"{field}'s value must be a list when using 'not in' comparator in Pinecone"
238
232
  raise FilterError(msg)
239
-
240
- return f"{field} IS NULL OR {field} != ALL(%s)", [value]
233
+ return SQL("{} IS NULL OR {} != ALL(%s)").format(field, field), [value]
241
234
 
242
235
 
243
- def _in(field: str, value: Any) -> tuple[str, list]:
236
+ def _in(field: Composable, value: Any) -> tuple[Composed, list]:
244
237
  if not isinstance(value, list):
245
238
  msg = f"{field}'s value must be a list when using 'in' comparator in Pinecone"
246
239
  raise FilterError(msg)
247
240
 
248
241
  # see https://www.psycopg.org/psycopg3/docs/basic/adapt.html#lists-adaptation
249
- return f"{field} = ANY(%s)", [value]
242
+ return SQL("{} = ANY(%s)").format(field), [value]
250
243
 
251
244
 
252
- def _like(field: str, value: Any) -> tuple[str, Any]:
245
+ def _like(field: Composable, value: Any) -> tuple[Composed, Any]:
253
246
  if not isinstance(value, str):
254
247
  msg = f"{field}'s value must be a str when using 'LIKE' "
255
248
  raise FilterError(msg)
256
- return f"{field} LIKE %s", value
249
+ return SQL("{} LIKE %s").format(field), value
257
250
 
258
251
 
259
- def _not_like(field: str, value: Any) -> tuple[str, Any]:
252
+ def _not_like(field: Composable, value: Any) -> tuple[Composed, Any]:
260
253
  if not isinstance(value, str):
261
254
  msg = f"{field}'s value must be a str when using 'LIKE' "
262
255
  raise FilterError(msg)
263
- return f"{field} NOT LIKE %s", value
256
+ return SQL("{} NOT LIKE %s").format(field), value
264
257
 
265
258
 
266
259
  COMPARISON_OPERATORS = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgvector-haystack
3
- Version: 6.2.0
3
+ Version: 6.3.0
4
4
  Summary: An integration of pgvector (vector search extension for Postgres) with Haystack
5
5
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations
6
6
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/pgvector/README.md
@@ -6,8 +6,8 @@ haystack_integrations/document_stores/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
6
6
  haystack_integrations/document_stores/pgvector/__init__.py,sha256=Ge-7ggarfr3x8UqNdFHUuFGiu52u49sOVe3l16WEwZU,197
7
7
  haystack_integrations/document_stores/pgvector/converters.py,sha256=SXLDpb-GfaKSHl5Oann38T27QWR-8pamTmdLklK9my0,3181
8
8
  haystack_integrations/document_stores/pgvector/document_store.py,sha256=nwxyI450JqN5rP50ZMWRs_2otT2J5kWuxbejteLhZUE,84473
9
- haystack_integrations/document_stores/pgvector/filters.py,sha256=SpfqFvSLhQOs42QPBJuWj63Zv-q6J1ja9xaXR36S2Pw,9300
10
- pgvector_haystack-6.2.0.dist-info/METADATA,sha256=DEi8WE03yyge9p0xI6AiRmwWtkQ2c96U5inGdg-hQ18,2095
11
- pgvector_haystack-6.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
- pgvector_haystack-6.2.0.dist-info/licenses/LICENSE.txt,sha256=B05uMshqTA74s-0ltyHKI6yoPfJ3zYgQbvcXfDVGFf8,10280
13
- pgvector_haystack-6.2.0.dist-info/RECORD,,
9
+ haystack_integrations/document_stores/pgvector/filters.py,sha256=tItWTbduXZo3R5tFf_nQ5d7x4HmCfUG_sb9Shpay_48,9663
10
+ pgvector_haystack-6.3.0.dist-info/METADATA,sha256=8t6F48PrKMKypQkqvlCDsAs3_0TeaclHB-O_pltZqcU,2095
11
+ pgvector_haystack-6.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ pgvector_haystack-6.3.0.dist-info/licenses/LICENSE.txt,sha256=B05uMshqTA74s-0ltyHKI6yoPfJ3zYgQbvcXfDVGFf8,10280
13
+ pgvector_haystack-6.3.0.dist-info/RECORD,,