From e3e04137470a84c4a7d386f1d4a2769884e67200 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Mon, 5 Oct 2026 10:47:53 +0700 Subject: [PATCH 1/2] Keep DuckDB bind values in query order when a parameter repeats --- src/python/txtai/database/duckdb.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/python/txtai/database/duckdb.py b/src/python/txtai/database/duckdb.py index 4c77028e..b2a51cee 100644 --- a/src/python/txtai/database/duckdb.py +++ b/src/python/txtai/database/duckdb.py @@ -139,20 +139,16 @@ class DuckDB(Embedded): # Unpack query args query, parameters = args - # Iterate over parameters - # - Replace named parameters with ?'s - # - Build list of value with position indexes + # Match all named parameters in a single pass, so values are listed in the order they appear in the query + # - Match on a word boundary, bind parameters can be followed by any non-word character, i.e. `in (:x)` params = [] - for key, value in parameters.items(): - # Match on a word boundary, bind parameters can be followed by any non-word character, i.e. `in (:x)` - pattern = rf"\:{key}\b" - for match in re.finditer(pattern, query): - params.append((match.start(), value)) - + if parameters: + pattern = r"\:(" + "|".join(re.escape(key) for key in parameters) + r")\b" + params = [parameters[match.group(1)] for match in re.finditer(pattern, query)] query = re.sub(pattern, "?", query) # Repack query and parameter list - args = (query, [value for _, value in sorted(params, key=lambda x: x[0])]) + args = (query, params) return args From 346bb9d282d237fa46dee53d860f13fa416d0ead Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Mon, 5 Oct 2026 11:12:23 +0700 Subject: [PATCH 2/2] Test a repeated bind parameter followed by another parameter --- test/python/testdatabase/testrdbms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/python/testdatabase/testrdbms.py b/test/python/testdatabase/testrdbms.py index 5a27a8a8..e0466edb 100644 --- a/test/python/testdatabase/testrdbms.py +++ b/test/python/testdatabase/testrdbms.py @@ -745,6 +745,13 @@ class Common: result = self.embeddings.search("select * from txtai where text in (:x, :y)", parameters={"x": self.data[1], "y": self.data[4]}) self.assertEqual({row["text"] for row in result}, {self.data[1], self.data[4]}) + # Test a repeated bind parameter followed by another bind parameter + result = self.embeddings.search( + "select * from txtai where text like :document or text like :document or text like :document or id = :id", + parameters={"document": "%iceberg%", "id": 4}, + ) + self.assertEqual({row["text"] for row in result}, {self.data[1], self.data[4]}) + def testSparse(self): """ Test sparse vector search