From 820210912182cb3b1c13b902c74d09dfd93376ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 19:58:08 +0100 Subject: [PATCH 01/14] Initial support for mariadb vector similarity --- CHANGELOG.md | 11 +++++ .../cli/commands/app/preloader/core.php | 1 + src/mako/database/query/Query.php | 49 +++++++++++++++---- src/mako/database/query/VectorMetric.php | 17 +++++++ .../database/query/compilers/Compiler.php | 8 +++ src/mako/database/query/compilers/MariaDB.php | 17 +++++++ .../query/compilers/MariaDBCompilerTest.php | 31 ++++++++++++ 7 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/mako/database/query/VectorMetric.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfca383c..a10a46331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +### 12.1.0 (2026-??-??) + +#### New + +* Added vector query support to the query builder for the following databases: + - `MariaDB` + - `MySQL` + - `Postgres` + +-------------------------------------------------------- + ### 11.4.6, 12.0.2 (2026-01-08) diff --git a/src/mako/application/cli/commands/app/preloader/core.php b/src/mako/application/cli/commands/app/preloader/core.php index cd6282cd9..1c32dc73f 100644 --- a/src/mako/application/cli/commands/app/preloader/core.php +++ b/src/mako/application/cli/commands/app/preloader/core.php @@ -78,6 +78,7 @@ mako\database\query\Result::class, mako\database\query\ResultSet::class, mako\database\query\Subquery::class, + mako\database\query\VectorMetric::class, mako\error\ErrorHandler::class, mako\file\FileSystem::class, mako\file\Permission::class, diff --git a/src/mako/database/query/Query.php b/src/mako/database/query/Query.php index 4cbaa76e5..796bc8d11 100644 --- a/src/mako/database/query/Query.php +++ b/src/mako/database/query/Query.php @@ -545,6 +545,16 @@ public function where(array|Closure|Raw|string $column, ?string $operator = null return $this; } + /** + * Adds a OR WHERE clause. + * + * @return $this + */ + public function orWhere(array|Closure|Raw|string $column, ?string $operator = null, mixed $value = null): static + { + return $this->where($column, $operator, $value, 'OR'); + } + /** * Adds a raw WHERE clause. * @@ -565,16 +575,6 @@ public function whereRaw(array|Raw|string $column, null|array|string $operator = return $this->where($column, $operator, new Raw($raw), $separator); } - /** - * Adds a OR WHERE clause. - * - * @return $this - */ - public function orWhere(array|Closure|Raw|string $column, ?string $operator = null, mixed $value = null): static - { - return $this->where($column, $operator, $value, 'OR'); - } - /** * Adds a raw OR WHERE clause. * @@ -642,6 +642,35 @@ public function orWhereColumn(array|string $column1, string $operator, array|str return $this->whereColumn($column1, $operator, $column2, 'OR'); } + /** + * Adds a vector similarity clause. + * + * @return $this + */ + public function whereVectorSimilarity(string $column, array|string $vector, float $minSimilarity = 0.8, VectorMetric $vectorMetric = VectorMetric::COSINE, string $separator = 'AND'): static + { + $this->wheres[] = [ + 'type' => 'whereVectorSimilarity', + 'column' => $column, + 'vector' => $vector, + 'similarity' => $minSimilarity, + 'metric' => $vectorMetric, + 'separator' => $separator, + ]; + + return $this; + } + + /** + * Adds a vector similarity clause. + * + * @return $this + */ + public function orWhereVectorSimilarity(string $column, array|string $vector, float $minSimilarity = 0.8, VectorMetric $vectorMetric = VectorMetric::COSINE): static + { + return $this->whereVectorSimilarity($column, $vector, $minSimilarity, $vectorMetric, 'OR'); + } + /** * Adds a BETWEEN clause. * diff --git a/src/mako/database/query/VectorMetric.php b/src/mako/database/query/VectorMetric.php new file mode 100644 index 000000000..3b0c3bcd3 --- /dev/null +++ b/src/mako/database/query/VectorMetric.php @@ -0,0 +1,17 @@ +columnName($where['column1'])} {$where['operator']} {$this->columnName($where['column2'])}"; } + /** + * Compiles vector similarity clauses. + */ + protected function whereVectorSimilarity(array $where): string + { + throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector similarity calculations.', static::class)); + } + /** * Compiles BETWEEN clauses. */ diff --git a/src/mako/database/query/compilers/MariaDB.php b/src/mako/database/query/compilers/MariaDB.php index ff6d6a214..065020374 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -7,6 +7,7 @@ namespace mako\database\query\compilers; +use mako\database\query\VectorMetric; use Override; /** @@ -14,6 +15,22 @@ */ class MariaDB extends MySQL { + /** + * {@inheritDoc} + */ + #[Override] + public function whereVectorSimilarity(array $where): string + { + $function = match ($where['metric']) { + VectorMetric::COSINE => 'VEC_DISTANCE_COSINE', + VectorMetric::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', + }; + + $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + + return "EXP(-{$function}({$this->column($where['column'], false)}, VEC_FromText({$this->param($vector)}))) >= {$this->param($where['similarity'])}"; + } + /** * {@inheritDoc} */ diff --git a/tests/unit/database/query/compilers/MariaDBCompilerTest.php b/tests/unit/database/query/compilers/MariaDBCompilerTest.php index 5cdbe7766..179f8b483 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -11,6 +11,7 @@ use mako\database\query\compilers\MariaDB as MariaDBCompiler; use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; +use mako\database\query\VectorMetric; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -43,6 +44,36 @@ protected function getBuilder($table = 'foobar') return (new Query($this->getConnection()))->table($table); } + /** + * + */ + public function testBasicCosineWhereVectorSimilarity(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` WHERE EXP(-VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?))) >= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + } + + /** + * + */ + public function testBasicEuclideanWhereVectorSimilarity(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75, vectorMetric: VectorMetric::EUCLIDEAN) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` WHERE EXP(-VEC_DISTANCE_EUCLIDEAN(`embedding`, VEC_FromText(?))) >= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + } + /** * */ From 4851a6c74f62d1cc33decb487b826c1ad4bb5fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 20:14:20 +0100 Subject: [PATCH 02/14] Initial support for mysql vector similarity --- src/mako/database/query/VectorMetric.php | 6 ++-- src/mako/database/query/compilers/MySQL.php | 17 ++++++++++ .../query/compilers/MySQLCompilerTest.php | 31 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/mako/database/query/VectorMetric.php b/src/mako/database/query/VectorMetric.php index 3b0c3bcd3..501139e88 100644 --- a/src/mako/database/query/VectorMetric.php +++ b/src/mako/database/query/VectorMetric.php @@ -10,8 +10,8 @@ /** * Vector metrics. */ -enum VectorMetric: string +enum VectorMetric { - case COSINE = 'COSINE'; - case EUCLIDEAN = 'EUCLIDEAN'; + case COSINE; + case EUCLIDEAN; } diff --git a/src/mako/database/query/compilers/MySQL.php b/src/mako/database/query/compilers/MySQL.php index b142cf595..3762f81ca 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -8,6 +8,7 @@ namespace mako\database\query\compilers; use mako\database\query\compilers\traits\JsonPathBuilderTrait; +use mako\database\query\VectorMetric; use Override; use function str_replace; @@ -52,6 +53,22 @@ protected function buildJsonSet(string $column, array $segments, string $param): return "{$column} = JSON_SET({$column}, '{$this->buildJsonPath($segments)}', CAST({$param} AS JSON))"; } + /** + * {@inheritDoc} + */ + #[Override] + public function whereVectorSimilarity(array $where): string + { + $metric = match ($where['metric']) { + VectorMetric::COSINE => 'COSINE', + VectorMetric::EUCLIDEAN => 'EUCLIDEAN', + }; + + $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + + return "EXP(-DISTANCE({$this->column($where['column'], false)}, STRING_TO_VECTOR({$this->param($vector)}), '{$metric}')) >= {$this->param($where['similarity'])}"; + } + /** * {@inheritDoc} */ diff --git a/tests/unit/database/query/compilers/MySQLCompilerTest.php b/tests/unit/database/query/compilers/MySQLCompilerTest.php index 35a14ad41..379c3b5c8 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -11,6 +11,7 @@ use mako\database\query\compilers\MySQL as MySQLCompiler; use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; +use mako\database\query\VectorMetric; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -232,6 +233,36 @@ public function testBasicInsertWithNoValues(): void $this->assertEquals([], $query['params']); } + /** + * + */ + public function testBasicCosineWhereVectorSimilarity(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` WHERE EXP(-DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE')) >= ?", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + } + + /** + * + */ + public function testBasicEuclideanWhereVectorSimilarity(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75, vectorMetric: VectorMetric::EUCLIDEAN) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` WHERE EXP(-DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'EUCLIDEAN')) >= ?", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + } + /** * */ From 80bce5bcba8845c3fb86b5c24c9a293581dbc117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 22:00:24 +0100 Subject: [PATCH 03/14] similarity -> distance --- src/mako/database/query/Query.php | 22 +++++++++---------- .../database/query/compilers/Compiler.php | 6 ++--- src/mako/database/query/compilers/MariaDB.php | 8 +++---- src/mako/database/query/compilers/MySQL.php | 8 +++---- .../query/compilers/MariaDBCompilerTest.php | 16 +++++++------- .../query/compilers/MySQLCompilerTest.php | 16 +++++++------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/mako/database/query/Query.php b/src/mako/database/query/Query.php index 796bc8d11..a968e4e6b 100644 --- a/src/mako/database/query/Query.php +++ b/src/mako/database/query/Query.php @@ -643,32 +643,32 @@ public function orWhereColumn(array|string $column1, string $operator, array|str } /** - * Adds a vector similarity clause. + * Adds a vector distance clause. * * @return $this */ - public function whereVectorSimilarity(string $column, array|string $vector, float $minSimilarity = 0.8, VectorMetric $vectorMetric = VectorMetric::COSINE, string $separator = 'AND'): static + public function whereVectorDistance(string $column, array|string $vector, float $maxDistance = 0.2, VectorMetric $vectorMetric = VectorMetric::COSINE, string $separator = 'AND'): static { $this->wheres[] = [ - 'type' => 'whereVectorSimilarity', - 'column' => $column, - 'vector' => $vector, - 'similarity' => $minSimilarity, - 'metric' => $vectorMetric, - 'separator' => $separator, + 'type' => 'whereVectorDistance', + 'column' => $column, + 'vector' => $vector, + 'distance' => $maxDistance, + 'metric' => $vectorMetric, + 'separator' => $separator, ]; return $this; } /** - * Adds a vector similarity clause. + * Adds a vector distance clause. * * @return $this */ - public function orWhereVectorSimilarity(string $column, array|string $vector, float $minSimilarity = 0.8, VectorMetric $vectorMetric = VectorMetric::COSINE): static + public function orWhereVectorDistance(string $column, array|string $vector, float $maxDistance = 0.2, VectorMetric $vectorMetric = VectorMetric::COSINE): static { - return $this->whereVectorSimilarity($column, $vector, $minSimilarity, $vectorMetric, 'OR'); + return $this->whereVectorDistance($column, $vector, $maxDistance, $vectorMetric, 'OR'); } /** diff --git a/src/mako/database/query/compilers/Compiler.php b/src/mako/database/query/compilers/Compiler.php index 3101e4e9e..198f92940 100644 --- a/src/mako/database/query/compilers/Compiler.php +++ b/src/mako/database/query/compilers/Compiler.php @@ -484,11 +484,11 @@ protected function whereColumn(array $where): string } /** - * Compiles vector similarity clauses. + * Compiles vector distance clauses. */ - protected function whereVectorSimilarity(array $where): string + protected function whereVectorDistance(array $where): string { - throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector similarity calculations.', static::class)); + throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector distance calculations.', static::class)); } /** diff --git a/src/mako/database/query/compilers/MariaDB.php b/src/mako/database/query/compilers/MariaDB.php index 065020374..61739d7ca 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -19,16 +19,16 @@ class MariaDB extends MySQL * {@inheritDoc} */ #[Override] - public function whereVectorSimilarity(array $where): string + public function whereVectorDistance(array $where): string { + $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + $function = match ($where['metric']) { VectorMetric::COSINE => 'VEC_DISTANCE_COSINE', VectorMetric::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', }; - $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; - - return "EXP(-{$function}({$this->column($where['column'], false)}, VEC_FromText({$this->param($vector)}))) >= {$this->param($where['similarity'])}"; + return "{$function}({$this->column($where['column'], false)}, VEC_FromText({$this->param($vector)})) <= {$this->param($where['distance'])}"; } /** diff --git a/src/mako/database/query/compilers/MySQL.php b/src/mako/database/query/compilers/MySQL.php index 3762f81ca..4da3213c0 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -57,16 +57,16 @@ protected function buildJsonSet(string $column, array $segments, string $param): * {@inheritDoc} */ #[Override] - public function whereVectorSimilarity(array $where): string + public function whereVectorDistance(array $where): string { + $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + $metric = match ($where['metric']) { VectorMetric::COSINE => 'COSINE', VectorMetric::EUCLIDEAN => 'EUCLIDEAN', }; - $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; - - return "EXP(-DISTANCE({$this->column($where['column'], false)}, STRING_TO_VECTOR({$this->param($vector)}), '{$metric}')) >= {$this->param($where['similarity'])}"; + return "DISTANCE({$this->column($where['column'], false)}, STRING_TO_VECTOR({$this->param($vector)}), '{$metric}') <= {$this->param($where['distance'])}"; } /** diff --git a/tests/unit/database/query/compilers/MariaDBCompilerTest.php b/tests/unit/database/query/compilers/MariaDBCompilerTest.php index 179f8b483..fb21b748d 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -47,31 +47,31 @@ protected function getBuilder($table = 'foobar') /** * */ - public function testBasicCosineWhereVectorSimilarity(): void + public function testBasicCosineWhereVectorDistance(): void { $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5) ->getCompiler()->select(); - $this->assertEquals('SELECT * FROM `foobar` WHERE EXP(-VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?))) >= ?', $query['sql']); - $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + $this->assertEquals('SELECT * FROM `foobar` WHERE VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) <= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } /** * */ - public function testBasicEuclideanWhereVectorSimilarity(): void + public function testBasicEuclidianWhereVectorDistance(): void { $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75, vectorMetric: VectorMetric::EUCLIDEAN) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorMetric: VectorMetric::EUCLIDEAN) ->getCompiler()->select(); - $this->assertEquals('SELECT * FROM `foobar` WHERE EXP(-VEC_DISTANCE_EUCLIDEAN(`embedding`, VEC_FromText(?))) >= ?', $query['sql']); - $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + $this->assertEquals('SELECT * FROM `foobar` WHERE VEC_DISTANCE_EUCLIDEAN(`embedding`, VEC_FromText(?)) <= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } /** diff --git a/tests/unit/database/query/compilers/MySQLCompilerTest.php b/tests/unit/database/query/compilers/MySQLCompilerTest.php index 379c3b5c8..a71213b89 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -236,31 +236,31 @@ public function testBasicInsertWithNoValues(): void /** * */ - public function testBasicCosineWhereVectorSimilarity(): void + public function testBasicCosineWhereVectorDistance(): void { $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5) ->getCompiler()->select(); - $this->assertEquals("SELECT * FROM `foobar` WHERE EXP(-DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE')) >= ?", $query['sql']); - $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + $this->assertEquals("SELECT * FROM `foobar` WHERE DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') <= ?", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } /** * */ - public function testBasicEuclideanWhereVectorSimilarity(): void + public function testBasicEuclidianWhereVectorDistance(): void { $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorSimilarity('embedding', [1, 2, 3, 4, 5], minSimilarity: 0.75, vectorMetric: VectorMetric::EUCLIDEAN) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorMetric: VectorMetric::EUCLIDEAN) ->getCompiler()->select(); - $this->assertEquals("SELECT * FROM `foobar` WHERE EXP(-DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'EUCLIDEAN')) >= ?", $query['sql']); - $this->assertEquals(['[1,2,3,4,5]', 0.75], $query['params']); + $this->assertEquals("SELECT * FROM `foobar` WHERE DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'EUCLIDEAN') <= ?", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } /** From 2257d604fa65598df270b973b67fb42042743d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 22:01:47 +0100 Subject: [PATCH 04/14] Cleaning up --- src/mako/database/query/compilers/MariaDB.php | 3 +++ src/mako/database/query/compilers/MySQL.php | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/mako/database/query/compilers/MariaDB.php b/src/mako/database/query/compilers/MariaDB.php index 61739d7ca..386c86ed3 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -10,6 +10,9 @@ use mako\database\query\VectorMetric; use Override; +use function is_array; +use function json_encode; + /** * Compiles MariaDB queries. */ diff --git a/src/mako/database/query/compilers/MySQL.php b/src/mako/database/query/compilers/MySQL.php index 4da3213c0..a2fc25c4a 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -11,6 +11,8 @@ use mako\database\query\VectorMetric; use Override; +use function is_array; +use function json_encode; use function str_replace; /** From b5d607c4e722cdc77fba2fa15300124f2ca961ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 22:12:56 +0100 Subject: [PATCH 05/14] Added vector distance support to postgres compiler --- .../database/query/compilers/Postgres.php | 19 ++++++++++++ .../query/compilers/PostgresCompilerTest.php | 31 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/mako/database/query/compilers/Postgres.php b/src/mako/database/query/compilers/Postgres.php index 1d5717eac..9bbae9e5f 100644 --- a/src/mako/database/query/compilers/Postgres.php +++ b/src/mako/database/query/compilers/Postgres.php @@ -7,11 +7,14 @@ namespace mako\database\query\compilers; +use mako\database\query\VectorMetric; use Override; use function array_pop; use function implode; +use function is_array; use function is_numeric; +use function json_encode; use function str_replace; /** @@ -55,6 +58,22 @@ protected function buildJsonSet(string $column, array $segments, string $param): return $column . " = JSONB_SET({$column}, '{" . str_replace("'", "''", implode(',', $segments)) . "}', '{$param}')"; } + /** + * {@inheritDoc} + */ + #[Override] + public function whereVectorDistance(array $where): string + { + $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + + $operator = match ($where['metric']) { + VectorMetric::COSINE => '<=>', + VectorMetric::EUCLIDEAN => '<->', + }; + + return "{$this->column($where['column'], false)} {$operator} {$this->param($vector)} <= {$this->param($where['distance'])}"; + } + /** * {@inheritDoc} */ diff --git a/tests/unit/database/query/compilers/PostgresCompilerTest.php b/tests/unit/database/query/compilers/PostgresCompilerTest.php index bccb430bd..4bf196d95 100644 --- a/tests/unit/database/query/compilers/PostgresCompilerTest.php +++ b/tests/unit/database/query/compilers/PostgresCompilerTest.php @@ -11,6 +11,7 @@ use mako\database\query\compilers\Postgres as PostgresCompiler; use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; +use mako\database\query\VectorMetric; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -204,6 +205,36 @@ public function testUpdateWithJSONColumn(): void $this->assertEquals([1], $query['params']); } + /** + * + */ + public function testBasicCosineWhereVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" WHERE "embedding" <=> ? <= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); + } + + /** + * + */ + public function testBasicEuclidianWhereVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorMetric: VectorMetric::EUCLIDEAN) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" WHERE "embedding" <-> ? <= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); + } + /** * */ From 108ac4f3713230a8e624095245f2519b07374003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 22:42:00 +0100 Subject: [PATCH 06/14] Add support for vectors from subqueries --- .../cli/commands/app/preloader/core.php | 2 +- src/mako/database/query/Query.php | 18 ++++----- .../{VectorMetric.php => VectorDistance.php} | 4 +- src/mako/database/query/compilers/MariaDB.php | 24 +++++++++--- src/mako/database/query/compilers/MySQL.php | 24 +++++++++--- .../database/query/compilers/Postgres.php | 24 +++++++++--- .../query/compilers/MariaDBCompilerTest.php | 37 ++++++++++++++++++- .../query/compilers/MySQLCompilerTest.php | 37 ++++++++++++++++++- .../query/compilers/PostgresCompilerTest.php | 37 ++++++++++++++++++- 9 files changed, 171 insertions(+), 36 deletions(-) rename src/mako/database/query/{VectorMetric.php => VectorDistance.php} (81%) diff --git a/src/mako/application/cli/commands/app/preloader/core.php b/src/mako/application/cli/commands/app/preloader/core.php index 1c32dc73f..8fe8cec00 100644 --- a/src/mako/application/cli/commands/app/preloader/core.php +++ b/src/mako/application/cli/commands/app/preloader/core.php @@ -78,7 +78,7 @@ mako\database\query\Result::class, mako\database\query\ResultSet::class, mako\database\query\Subquery::class, - mako\database\query\VectorMetric::class, + mako\database\query\VectorDistance::class, mako\error\ErrorHandler::class, mako\file\FileSystem::class, mako\file\Permission::class, diff --git a/src/mako/database/query/Query.php b/src/mako/database/query/Query.php index a968e4e6b..2837e492f 100644 --- a/src/mako/database/query/Query.php +++ b/src/mako/database/query/Query.php @@ -647,15 +647,15 @@ public function orWhereColumn(array|string $column1, string $operator, array|str * * @return $this */ - public function whereVectorDistance(string $column, array|string $vector, float $maxDistance = 0.2, VectorMetric $vectorMetric = VectorMetric::COSINE, string $separator = 'AND'): static + public function whereVectorDistance(string $column, array|string|Subquery $vector, float $maxDistance = 0.2, VectorDistance $vectorDistance = VectorDistance::COSINE, string $separator = 'AND'): static { $this->wheres[] = [ - 'type' => 'whereVectorDistance', - 'column' => $column, - 'vector' => $vector, - 'distance' => $maxDistance, - 'metric' => $vectorMetric, - 'separator' => $separator, + 'type' => 'whereVectorDistance', + 'column' => $column, + 'vector' => $vector, + 'maxDistance' => $maxDistance, + 'vectorDistance' => $vectorDistance, + 'separator' => $separator, ]; return $this; @@ -666,9 +666,9 @@ public function whereVectorDistance(string $column, array|string $vector, float * * @return $this */ - public function orWhereVectorDistance(string $column, array|string $vector, float $maxDistance = 0.2, VectorMetric $vectorMetric = VectorMetric::COSINE): static + public function orWhereVectorDistance(string $column, array|string|Subquery $vector, float $maxDistance = 0.2, VectorDistance $vectorDistance = VectorDistance::COSINE): static { - return $this->whereVectorDistance($column, $vector, $maxDistance, $vectorMetric, 'OR'); + return $this->whereVectorDistance($column, $vector, $maxDistance, $vectorDistance, 'OR'); } /** diff --git a/src/mako/database/query/VectorMetric.php b/src/mako/database/query/VectorDistance.php similarity index 81% rename from src/mako/database/query/VectorMetric.php rename to src/mako/database/query/VectorDistance.php index 501139e88..52a23b6dd 100644 --- a/src/mako/database/query/VectorMetric.php +++ b/src/mako/database/query/VectorDistance.php @@ -8,9 +8,9 @@ namespace mako\database\query; /** - * Vector metrics. + * Vector distance. */ -enum VectorMetric +enum VectorDistance { case COSINE; case EUCLIDEAN; diff --git a/src/mako/database/query/compilers/MariaDB.php b/src/mako/database/query/compilers/MariaDB.php index 386c86ed3..a84797d1e 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -7,7 +7,8 @@ namespace mako\database\query\compilers; -use mako\database\query\VectorMetric; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use Override; use function is_array; @@ -24,14 +25,25 @@ class MariaDB extends MySQL #[Override] public function whereVectorDistance(array $where): string { - $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + $vector = $where['vector']; - $function = match ($where['metric']) { - VectorMetric::COSINE => 'VEC_DISTANCE_COSINE', - VectorMetric::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', + if ($vector instanceof Subquery) { + $vector = $this->subquery($vector); + } + else { + if (is_array($vector)) { + $vector = json_encode($vector); + } + + $vector = "VEC_FromText({$this->param($vector)})"; + } + + $function = match ($where['vectorDistance']) { + VectorDistance::COSINE => 'VEC_DISTANCE_COSINE', + VectorDistance::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', }; - return "{$function}({$this->column($where['column'], false)}, VEC_FromText({$this->param($vector)})) <= {$this->param($where['distance'])}"; + return "{$function}({$this->column($where['column'], false)}, {$vector}) <= {$this->param($where['maxDistance'])}"; } /** diff --git a/src/mako/database/query/compilers/MySQL.php b/src/mako/database/query/compilers/MySQL.php index a2fc25c4a..1dbd7deda 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -8,7 +8,8 @@ namespace mako\database\query\compilers; use mako\database\query\compilers\traits\JsonPathBuilderTrait; -use mako\database\query\VectorMetric; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use Override; use function is_array; @@ -61,14 +62,25 @@ protected function buildJsonSet(string $column, array $segments, string $param): #[Override] public function whereVectorDistance(array $where): string { - $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + $vector = $where['vector']; - $metric = match ($where['metric']) { - VectorMetric::COSINE => 'COSINE', - VectorMetric::EUCLIDEAN => 'EUCLIDEAN', + if ($vector instanceof Subquery) { + $vector = $this->subquery($vector); + } + else { + if (is_array($vector)) { + $vector = json_encode($vector); + } + + $vector = "STRING_TO_VECTOR({$this->param($vector)})"; + } + + $vectorDistance = match ($where['vectorDistance']) { + VectorDistance::COSINE => 'COSINE', + VectorDistance::EUCLIDEAN => 'EUCLIDEAN', }; - return "DISTANCE({$this->column($where['column'], false)}, STRING_TO_VECTOR({$this->param($vector)}), '{$metric}') <= {$this->param($where['distance'])}"; + return "DISTANCE({$this->column($where['column'], false)}, {$vector}, '{$vectorDistance}') <= {$this->param($where['maxDistance'])}"; } /** diff --git a/src/mako/database/query/compilers/Postgres.php b/src/mako/database/query/compilers/Postgres.php index 9bbae9e5f..ed6b4dd3a 100644 --- a/src/mako/database/query/compilers/Postgres.php +++ b/src/mako/database/query/compilers/Postgres.php @@ -7,7 +7,8 @@ namespace mako\database\query\compilers; -use mako\database\query\VectorMetric; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use Override; use function array_pop; @@ -64,14 +65,25 @@ protected function buildJsonSet(string $column, array $segments, string $param): #[Override] public function whereVectorDistance(array $where): string { - $vector = is_array($where['vector']) ? json_encode($where['vector']) : $where['vector']; + $vector = $where['vector']; - $operator = match ($where['metric']) { - VectorMetric::COSINE => '<=>', - VectorMetric::EUCLIDEAN => '<->', + if ($vector instanceof Subquery) { + $vector = $this->subquery($vector); + } + else { + if (is_array($vector)) { + $vector = json_encode($vector); + } + + $vector = $this->param($vector); + } + + $operator = match ($where['vectorDistance']) { + VectorDistance::COSINE => '<=>', + VectorDistance::EUCLIDEAN => '<->', }; - return "{$this->column($where['column'], false)} {$operator} {$this->param($vector)} <= {$this->param($where['distance'])}"; + return "{$this->column($where['column'], false)} {$operator} {$vector} <= {$this->param($where['maxDistance'])}"; } /** diff --git a/tests/unit/database/query/compilers/MariaDBCompilerTest.php b/tests/unit/database/query/compilers/MariaDBCompilerTest.php index fb21b748d..6d0d19378 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -11,7 +11,8 @@ use mako\database\query\compilers\MariaDB as MariaDBCompiler; use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; -use mako\database\query\VectorMetric; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -67,13 +68,45 @@ public function testBasicEuclidianWhereVectorDistance(): void $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorMetric: VectorMetric::EUCLIDEAN) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorDistance: VectorDistance::EUCLIDEAN) ->getCompiler()->select(); $this->assertEquals('SELECT * FROM `foobar` WHERE VEC_DISTANCE_EUCLIDEAN(`embedding`, VEC_FromText(?)) <= ?', $query['sql']); $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } + /** + * + */ + public function testBasicCosineWhereVectorDistanceWithStringVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', '[1,2,3,4,5]', maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` WHERE VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) <= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); + } + + /** + * + */ + public function testCosineWhereVectorDistanceFromSubquery(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', new Subquery(function (Query $query): void { + $query->table('embeddings')->select(['embedding'])->where('id', '=', 1); + }), maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` WHERE VEC_DISTANCE_COSINE(`embedding`, (SELECT `embedding` FROM `embeddings` WHERE `id` = ?)) <= ?', $query['sql']); + $this->assertEquals([1, 0.5], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/MySQLCompilerTest.php b/tests/unit/database/query/compilers/MySQLCompilerTest.php index a71213b89..b999f7df0 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -11,7 +11,8 @@ use mako\database\query\compilers\MySQL as MySQLCompiler; use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; -use mako\database\query\VectorMetric; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -256,13 +257,45 @@ public function testBasicEuclidianWhereVectorDistance(): void $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorMetric: VectorMetric::EUCLIDEAN) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorDistance: VectorDistance::EUCLIDEAN) ->getCompiler()->select(); $this->assertEquals("SELECT * FROM `foobar` WHERE DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'EUCLIDEAN') <= ?", $query['sql']); $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } + /** + * + */ + public function testBasicCosineWhereVectorDistanceWithStringVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', '[1,2,3,4,5]', maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` WHERE DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') <= ?", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); + } + + /** + * + */ + public function testCosineWhereVectorDistanceFromSubquery(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', new Subquery(function (Query $query): void { + $query->table('embeddings')->select(['embedding'])->where('id', '=', 1); + }), maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` WHERE DISTANCE(`embedding`, (SELECT `embedding` FROM `embeddings` WHERE `id` = ?), 'COSINE') <= ?", $query['sql']); + $this->assertEquals([1, 0.5], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/PostgresCompilerTest.php b/tests/unit/database/query/compilers/PostgresCompilerTest.php index 4bf196d95..ea638fbfa 100644 --- a/tests/unit/database/query/compilers/PostgresCompilerTest.php +++ b/tests/unit/database/query/compilers/PostgresCompilerTest.php @@ -11,7 +11,8 @@ use mako\database\query\compilers\Postgres as PostgresCompiler; use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; -use mako\database\query\VectorMetric; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -228,13 +229,45 @@ public function testBasicEuclidianWhereVectorDistance(): void $query = $this->getBuilder(); $query = $query->table('foobar') - ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorMetric: VectorMetric::EUCLIDEAN) + ->whereVectorDistance('embedding', [1, 2, 3, 4, 5], maxDistance: 0.5, vectorDistance: VectorDistance::EUCLIDEAN) ->getCompiler()->select(); $this->assertEquals('SELECT * FROM "foobar" WHERE "embedding" <-> ? <= ?', $query['sql']); $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); } + /** + * + */ + public function testCosineWhereVectorDistanceStringVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', '[1,2,3,4,5]', maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" WHERE "embedding" <=> ? <= ?', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]', 0.5], $query['params']); + } + + /** + * + */ + public function testCosineWhereVectorDistanceFromSubquery(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->whereVectorDistance('embedding', new Subquery(function (Query $query): void { + $query->table('embeddings')->select(['embedding'])->where('id', '=', 1); + }), maxDistance: 0.5) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" WHERE "embedding" <=> (SELECT "embedding" FROM "embeddings" WHERE "id" = ?) <= ?', $query['sql']); + $this->assertEquals([1, 0.5], $query['params']); + } + /** * */ From aa63a6b0b7b063459d42250738b01b657cd0fa25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 23:11:36 +0100 Subject: [PATCH 07/14] Update Mako.php --- src/mako/Mako.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mako/Mako.php b/src/mako/Mako.php index b1aa0e11b..468a574c7 100644 --- a/src/mako/Mako.php +++ b/src/mako/Mako.php @@ -15,7 +15,7 @@ final class Mako /** * Mako version. */ - public const string VERSION = '12.0.2'; + public const string VERSION = '12.1.0'; /** * Mako major version. @@ -25,10 +25,10 @@ final class Mako /** * Mako minor version. */ - public const int VERSION_MINOR = 0; + public const int VERSION_MINOR = 1; /** * Mako patch version. */ - public const int VERSION_PATCH = 2; + public const int VERSION_PATCH = 0; } From f2be62726d63c4861191234f2b0e1924500ba850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Mon, 9 Feb 2026 23:53:42 +0100 Subject: [PATCH 08/14] Cleaning up --- src/mako/database/query/compilers/MariaDB.php | 20 +++++++++++++------ src/mako/database/query/compilers/MySQL.php | 20 +++++++++++++------ .../database/query/compilers/Postgres.php | 20 +++++++++++++------ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/mako/database/query/compilers/MariaDB.php b/src/mako/database/query/compilers/MariaDB.php index a84797d1e..eddb7f6bf 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -20,12 +20,11 @@ class MariaDB extends MySQL { /** - * {@inheritDoc} + * Returns a vector distance calculation. */ - #[Override] - public function whereVectorDistance(array $where): string + protected function vectorDistance(array $vectorDistance): string { - $vector = $where['vector']; + $vector = $vectorDistance['vector']; if ($vector instanceof Subquery) { $vector = $this->subquery($vector); @@ -38,12 +37,21 @@ public function whereVectorDistance(array $where): string $vector = "VEC_FromText({$this->param($vector)})"; } - $function = match ($where['vectorDistance']) { + $function = match ($vectorDistance['vectorDistance']) { VectorDistance::COSINE => 'VEC_DISTANCE_COSINE', VectorDistance::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', }; - return "{$function}({$this->column($where['column'], false)}, {$vector}) <= {$this->param($where['maxDistance'])}"; + return "{$function}({$this->column($vectorDistance['column'], false)}, {$vector})"; + } + + /** + * {@inheritDoc} + */ + #[Override] + protected function whereVectorDistance(array $where): string + { + return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}"; } /** diff --git a/src/mako/database/query/compilers/MySQL.php b/src/mako/database/query/compilers/MySQL.php index 1dbd7deda..16d89b304 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -57,12 +57,11 @@ protected function buildJsonSet(string $column, array $segments, string $param): } /** - * {@inheritDoc} + * Returns a vector distance calculation. */ - #[Override] - public function whereVectorDistance(array $where): string + protected function vectorDistance(array $vectorDistance): string { - $vector = $where['vector']; + $vector = $vectorDistance['vector']; if ($vector instanceof Subquery) { $vector = $this->subquery($vector); @@ -75,12 +74,21 @@ public function whereVectorDistance(array $where): string $vector = "STRING_TO_VECTOR({$this->param($vector)})"; } - $vectorDistance = match ($where['vectorDistance']) { + $function = match ($vectorDistance['vectorDistance']) { VectorDistance::COSINE => 'COSINE', VectorDistance::EUCLIDEAN => 'EUCLIDEAN', }; - return "DISTANCE({$this->column($where['column'], false)}, {$vector}, '{$vectorDistance}') <= {$this->param($where['maxDistance'])}"; + return "DISTANCE({$this->column($vectorDistance['column'], false)}, {$vector}, '{$function}')"; + } + + /** + * {@inheritDoc} + */ + #[Override] + protected function whereVectorDistance(array $where): string + { + return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}"; } /** diff --git a/src/mako/database/query/compilers/Postgres.php b/src/mako/database/query/compilers/Postgres.php index ed6b4dd3a..95427d65f 100644 --- a/src/mako/database/query/compilers/Postgres.php +++ b/src/mako/database/query/compilers/Postgres.php @@ -60,12 +60,11 @@ protected function buildJsonSet(string $column, array $segments, string $param): } /** - * {@inheritDoc} + * Returns a vector distance calculation. */ - #[Override] - public function whereVectorDistance(array $where): string + protected function vectorDistance(array $vectorDistance): string { - $vector = $where['vector']; + $vector = $vectorDistance['vector']; if ($vector instanceof Subquery) { $vector = $this->subquery($vector); @@ -78,12 +77,21 @@ public function whereVectorDistance(array $where): string $vector = $this->param($vector); } - $operator = match ($where['vectorDistance']) { + $function = match ($vectorDistance['vectorDistance']) { VectorDistance::COSINE => '<=>', VectorDistance::EUCLIDEAN => '<->', }; - return "{$this->column($where['column'], false)} {$operator} {$vector} <= {$this->param($where['maxDistance'])}"; + return "{$this->column($vectorDistance['column'], false)} {$function} {$vector}"; + } + + /** + * {@inheritDoc} + */ + #[Override] + protected function whereVectorDistance(array $where): string + { + return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}"; } /** From dec7736322a36a4246a46c758c28e420faf9527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Tue, 10 Feb 2026 17:21:57 +0100 Subject: [PATCH 09/14] Add support for sorting by vector distance --- src/mako/database/query/Query.php | 33 +++++++ .../database/query/compilers/Compiler.php | 18 +++- src/mako/database/query/compilers/MariaDB.php | 9 ++ src/mako/database/query/compilers/MySQL.php | 9 ++ .../database/query/compilers/Postgres.php | 9 ++ tests/unit/database/midgard/QueryTest.php | 4 +- .../query/compilers/MariaDBCompilerTest.php | 92 +++++++++++++++++++ .../query/compilers/MySQLCompilerTest.php | 92 +++++++++++++++++++ .../query/compilers/PostgresCompilerTest.php | 92 +++++++++++++++++++ 9 files changed, 355 insertions(+), 3 deletions(-) diff --git a/src/mako/database/query/Query.php b/src/mako/database/query/Query.php index 2837e492f..87dcf6833 100644 --- a/src/mako/database/query/Query.php +++ b/src/mako/database/query/Query.php @@ -1069,6 +1069,7 @@ public function orHavingRaw(string $raw, string $operator, mixed $value): static public function orderBy(array|Raw|string $columns, string $order = 'ASC'): static { $this->orderings[] = [ + 'type' => 'basicOrdering', 'column' => is_array($columns) ? $columns : [$columns], 'order' => ($order === 'ASC' || $order === 'asc') ? 'ASC' : 'DESC', ]; @@ -1126,6 +1127,38 @@ public function descendingRaw(string $raw, array $parameters = []): static return $this->orderByRaw($raw, $parameters, 'DESC'); } + /** + * Adds a vector ORDER BY clause. + */ + public function orderByVectorDistance(string $column, array|string|Subquery $vector, VectorDistance $vectorDistance = VectorDistance::COSINE, string $order = 'ASC'): static + { + $this->orderings[] = [ + 'type' => 'vectorDistanceOrdering', + 'column' => $column, + 'vector' => $vector, + 'vectorDistance' => $vectorDistance, + 'order' => ($order === 'ASC' || $order === 'asc') ? 'ASC' : 'DESC', + ]; + + return $this; + } + + /** + * Adds a ascending vector ORDER BY clause. + */ + public function ascendingVectorDistance(string $column, array|string|Subquery $vector, VectorDistance $vectorDistance = VectorDistance::COSINE): static + { + return $this->orderByVectorDistance($column, $vector, $vectorDistance, 'ASC'); + } + + /** + * Adds a descending vector ORDER BY clause. + */ + public function descendingVectorDistance(string $column, array|string|Subquery $vector, VectorDistance $vectorDistance = VectorDistance::COSINE): static + { + return $this->orderByVectorDistance($column, $vector, $vectorDistance, 'DESC'); + } + /** * Clears the ordering clauses. * diff --git a/src/mako/database/query/compilers/Compiler.php b/src/mako/database/query/compilers/Compiler.php index 198f92940..60d59ed1e 100644 --- a/src/mako/database/query/compilers/Compiler.php +++ b/src/mako/database/query/compilers/Compiler.php @@ -639,6 +639,22 @@ protected function groupings(array $groupings): string return empty($groupings) ? '' : " GROUP BY {$this->columns($groupings)}"; } + /** + * Compiles a basic ordering clause. + */ + protected function basicOrdering(array $order): string + { + return "{$this->columns($order['column'])} {$order['order']}"; + } + + /** + * Compiles vector distance ordering clause. + */ + protected function vectorDistanceOrdering(array $order): string + { + throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector distance calculations.', static::class)); + } + /** * Compiles ORDER BY clauses. */ @@ -651,7 +667,7 @@ protected function orderings(array $orderings): string $sql = []; foreach ($orderings as $order) { - $sql[] = "{$this->columns($order['column'])} {$order['order']}"; + $sql[] = $this->{$order['type']}($order); } return ' ORDER BY ' . implode(', ', $sql); diff --git a/src/mako/database/query/compilers/MariaDB.php b/src/mako/database/query/compilers/MariaDB.php index eddb7f6bf..bc9074f54 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -54,6 +54,15 @@ protected function whereVectorDistance(array $where): string return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}"; } + /** + * {@inheritDoc} + */ + #[Override] + protected function vectorDistanceOrdering(array $order): string + { + return "{$this->vectorDistance($order)} {$order['order']}"; + } + /** * {@inheritDoc} */ diff --git a/src/mako/database/query/compilers/MySQL.php b/src/mako/database/query/compilers/MySQL.php index 16d89b304..8cc81fde5 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -91,6 +91,15 @@ protected function whereVectorDistance(array $where): string return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}"; } + /** + * {@inheritDoc} + */ + #[Override] + protected function vectorDistanceOrdering(array $order): string + { + return "{$this->vectorDistance($order)} {$order['order']}"; + } + /** * {@inheritDoc} */ diff --git a/src/mako/database/query/compilers/Postgres.php b/src/mako/database/query/compilers/Postgres.php index 95427d65f..39e73b51b 100644 --- a/src/mako/database/query/compilers/Postgres.php +++ b/src/mako/database/query/compilers/Postgres.php @@ -94,6 +94,15 @@ protected function whereVectorDistance(array $where): string return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}"; } + /** + * {@inheritDoc} + */ + #[Override] + protected function vectorDistanceOrdering(array $order): string + { + return "{$this->vectorDistance($order)} {$order['order']}"; + } + /** * {@inheritDoc} */ diff --git a/tests/unit/database/midgard/QueryTest.php b/tests/unit/database/midgard/QueryTest.php index 20357f881..939fca02a 100644 --- a/tests/unit/database/midgard/QueryTest.php +++ b/tests/unit/database/midgard/QueryTest.php @@ -404,7 +404,7 @@ public function testBatch(): void }); - $this->assertEquals([['column' => ['foobar'], 'order' => 'ASC']], $query->getOrderings()); + $this->assertEquals([['type' => 'basicOrdering', 'column' => ['foobar'], 'order' => 'ASC']], $query->getOrderings()); // @@ -419,6 +419,6 @@ public function testBatch(): void }); - $this->assertEquals([['column' => ['barfoo'], 'order' => 'DESC']], $query->getOrderings()); + $this->assertEquals([['type' => 'basicOrdering', 'column' => ['barfoo'], 'order' => 'DESC']], $query->getOrderings()); } } diff --git a/tests/unit/database/query/compilers/MariaDBCompilerTest.php b/tests/unit/database/query/compilers/MariaDBCompilerTest.php index 6d0d19378..cea9a70f6 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -107,6 +107,98 @@ public function testCosineWhereVectorDistanceFromSubquery(): void $this->assertEquals([1, 0.5], $query['params']); } + /** + * + */ + public function testOrderByVectorDistanceCosine(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` ORDER BY VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceCosineWithStringVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', '[1,2,3,4,5]') + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` ORDER BY VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceCosineWithSubqueryVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', new Subquery(function (Query $query): void { + $query->table('embeddings')->select(['embedding'])->where('id', '=', 1); + })) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` ORDER BY VEC_DISTANCE_COSINE(`embedding`, (SELECT `embedding` FROM `embeddings` WHERE `id` = ?)) ASC', $query['sql']); + $this->assertEquals([1], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceEuclidean(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', [1, 2, 3, 4, 5], VectorDistance::EUCLIDEAN) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` ORDER BY VEC_DISTANCE_EUCLIDEAN(`embedding`, VEC_FromText(?)) ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testAscendingVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->ascendingVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` ORDER BY VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testDescendingVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->descendingVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM `foobar` ORDER BY VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) DESC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/MySQLCompilerTest.php b/tests/unit/database/query/compilers/MySQLCompilerTest.php index b999f7df0..1b5e9c12e 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -296,6 +296,98 @@ public function testCosineWhereVectorDistanceFromSubquery(): void $this->assertEquals([1, 0.5], $query['params']); } + /** + * + */ + public function testOrderByVectorDistanceCosine(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` ORDER BY DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') ASC", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceCosineWithStringVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', '[1,2,3,4,5]') + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` ORDER BY DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') ASC", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceCosineWithSubqueryVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', new Subquery(function (Query $query): void { + $query->table('embeddings')->select(['embedding'])->where('id', '=', 1); + })) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` ORDER BY DISTANCE(`embedding`, (SELECT `embedding` FROM `embeddings` WHERE `id` = ?), 'COSINE') ASC", $query['sql']); + $this->assertEquals([1], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceEuclidean(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', [1, 2, 3, 4, 5], VectorDistance::EUCLIDEAN) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` ORDER BY DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'EUCLIDEAN') ASC", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testAscendingVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->ascendingVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` ORDER BY DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') ASC", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testDescendingVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->descendingVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals("SELECT * FROM `foobar` ORDER BY DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') DESC", $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/PostgresCompilerTest.php b/tests/unit/database/query/compilers/PostgresCompilerTest.php index ea638fbfa..15b999d5f 100644 --- a/tests/unit/database/query/compilers/PostgresCompilerTest.php +++ b/tests/unit/database/query/compilers/PostgresCompilerTest.php @@ -268,6 +268,98 @@ public function testCosineWhereVectorDistanceFromSubquery(): void $this->assertEquals([1, 0.5], $query['params']); } + /** + * + */ + public function testOrderByVectorDistanceCosine(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" ORDER BY "embedding" <=> ? ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceCosineWithStringVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', '[1,2,3,4,5]') + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" ORDER BY "embedding" <=> ? ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceCosineWithSubqueryVector(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', new Subquery(function (Query $query): void { + $query->table('embeddings')->select(['embedding'])->where('id', '=', 1); + })) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" ORDER BY "embedding" <=> (SELECT "embedding" FROM "embeddings" WHERE "id" = ?) ASC', $query['sql']); + $this->assertEquals([1], $query['params']); + } + + /** + * + */ + public function testOrderByVectorDistanceEuclidean(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->orderByVectorDistance('embedding', [1, 2, 3, 4, 5], VectorDistance::EUCLIDEAN) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" ORDER BY "embedding" <-> ? ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testAscendingVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->ascendingVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" ORDER BY "embedding" <=> ? ASC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + + /** + * + */ + public function testDescendingVectorDistance(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->descendingVectorDistance('embedding', [1, 2, 3, 4, 5]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT * FROM "foobar" ORDER BY "embedding" <=> ? DESC', $query['sql']); + $this->assertEquals(['[1,2,3,4,5]'], $query['params']); + } + /** * */ From 73dce09f5af58aef9a9fb2a1805c45d1872170ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Tue, 10 Feb 2026 19:18:23 +0100 Subject: [PATCH 10/14] Made it easier to insert and retrieve vectors --- .../database/query/compilers/Compiler.php | 59 ++++++++++++++---- .../database/query/values/ValueInterface.php | 26 ++++++++ src/mako/database/query/values/in/Vector.php | 54 +++++++++++++++++ src/mako/database/query/values/out/Value.php | 36 +++++++++++ .../values/out/ValueWithAliasInterface.php | 28 +++++++++ src/mako/database/query/values/out/Vector.php | 50 ++++++++++++++++ .../query/compilers/MariaDBCompilerTest.php | 60 +++++++++++++++++++ .../query/compilers/MySQLCompilerTest.php | 60 +++++++++++++++++++ .../query/compilers/PostgresCompilerTest.php | 60 +++++++++++++++++++ 9 files changed, 421 insertions(+), 12 deletions(-) create mode 100644 src/mako/database/query/values/ValueInterface.php create mode 100644 src/mako/database/query/values/in/Vector.php create mode 100644 src/mako/database/query/values/out/Value.php create mode 100644 src/mako/database/query/values/out/ValueWithAliasInterface.php create mode 100644 src/mako/database/query/values/out/Vector.php diff --git a/src/mako/database/query/compilers/Compiler.php b/src/mako/database/query/compilers/Compiler.php index 60d59ed1e..5b9ae6997 100644 --- a/src/mako/database/query/compilers/Compiler.php +++ b/src/mako/database/query/compilers/Compiler.php @@ -13,6 +13,8 @@ use mako\database\query\Query; use mako\database\query\Raw; use mako\database\query\Subquery; +use mako\database\query\values\out\ValueWithAliasInterface; +use mako\database\query\values\ValueInterface; use function array_keys; use function array_shift; @@ -83,6 +85,28 @@ protected function raw(Raw $raw): string return $raw->getSql(); } + /** + * Compiles a value. + */ + protected function value(ValueInterface $value): string + { + $parameters = $value->getParameters(); + + if (!empty($parameters)) { + $this->params = [...$this->params, ...$parameters]; + } + + $sql = $value->getSql($this); + + if ($value instanceof ValueWithAliasInterface) { + if (($alias = $value->getAlias()) !== null) { + $sql .= " AS {$this->escapeIdentifier($alias)}"; + } + } + + return $sql; + } + /** * Compiles a subselect and merges the parameters. */ @@ -210,11 +234,13 @@ public function escapeTableNames(array $tables): string */ public function table(Raw|string|Subquery $table): string { - if ($table instanceof Raw) { - return $this->raw($table); - } - elseif ($table instanceof Subquery) { - return $this->subquery($table); + if (is_object($table)) { + if ($table instanceof Raw) { + return $this->raw($table); + } + elseif ($table instanceof Subquery) { + return $this->subquery($table); + } } return $this->escapeTableNameWithAlias($table); @@ -286,15 +312,21 @@ public function columnNames(array $columns): string /** * Compiles a column. */ - public function column(Raw|string|Subquery $column, bool $allowAlias = false): string + public function column(Raw|string|Subquery|ValueInterface $column, bool $allowAlias = false): string { - if ($column instanceof Raw) { - return $this->raw($column); - } - elseif ($column instanceof Subquery) { - return $this->subquery($column); + if (is_object($column)) { + if ($column instanceof Raw) { + return $this->raw($column); + } + elseif ($column instanceof Subquery) { + return $this->subquery($column); + } + elseif ($column instanceof ValueInterface) { + return $this->value($column); + } } - elseif ($allowAlias && stripos($column, ' AS ') !== false) { + + if ($allowAlias && stripos($column, ' AS ') !== false) { [$column, , $alias] = explode(' ', $column, 3); return "{$this->columnName($column)} AS {$this->columnName($alias)}"; @@ -378,6 +410,9 @@ protected function param(mixed $param, bool $enclose = true): string return '?'; } + elseif ($param instanceof ValueInterface) { + return $this->value($param); + } } $this->params[] = $param; diff --git a/src/mako/database/query/values/ValueInterface.php b/src/mako/database/query/values/ValueInterface.php new file mode 100644 index 000000000..e2e91ae20 --- /dev/null +++ b/src/mako/database/query/values/ValueInterface.php @@ -0,0 +1,26 @@ + 'VEC_FromText(?)', + MySQL::class => 'STRING_TO_VECTOR(?)', + Postgres::class => '?', + default => throw new DatabaseException(sprintf('Vector values are not supported by the [ %s ] compiler.', $compiler::class)), + }; + } + + /** + * {@inheritDoc} + */ + #[Override] + public function getParameters(): ?array + { + return [ + is_array($this->vector) ? json_encode($this->vector) : $this->vector, + ]; + } +} diff --git a/src/mako/database/query/values/out/Value.php b/src/mako/database/query/values/out/Value.php new file mode 100644 index 000000000..db71a238e --- /dev/null +++ b/src/mako/database/query/values/out/Value.php @@ -0,0 +1,36 @@ +alias = $alias; + + return $this; + } + + /** + * Returns the value alias. + */ + public function getAlias(): ?string + { + return $this->alias; + } +} diff --git a/src/mako/database/query/values/out/ValueWithAliasInterface.php b/src/mako/database/query/values/out/ValueWithAliasInterface.php new file mode 100644 index 000000000..908d9ac02 --- /dev/null +++ b/src/mako/database/query/values/out/ValueWithAliasInterface.php @@ -0,0 +1,28 @@ + "VEC_ToText({$compiler->escapeColumnName($this->column)})", + MySQL::class => "VECTOR_TO_STRING({$compiler->escapeColumnName($this->column)})", + Postgres::class => "{$compiler->escapeColumnName($this->column)}", + default => throw new DatabaseException(sprintf('Vector values are not supported by the [ %s ] compiler.', $compiler::class)), + }; + } + + /** + * {@inheritDoc} + */ + #[Override] + public function getParameters(): ?array + { + return null; + } +} diff --git a/tests/unit/database/query/compilers/MariaDBCompilerTest.php b/tests/unit/database/query/compilers/MariaDBCompilerTest.php index cea9a70f6..beef84bb3 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -12,6 +12,8 @@ use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; use mako\database\query\Subquery; +use mako\database\query\values\in\Vector as InVector; +use mako\database\query\values\out\Vector as OutVector; use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; @@ -199,6 +201,64 @@ public function testDescendingVectorDistance(): void $this->assertEquals(['[1,2,3,4,5]'], $query['params']); } + /** + * + */ + public function testVectorSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVector('embedding')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VEC_ToText(`embedding`) FROM `foobar`', $query['sql']); + $this->assertEquals([], $query['params']); + } + + /** + * + */ + public function testVectorSelectValueWithAlias(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVector('embedding')->as('vector')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VEC_ToText(`embedding`) AS `vector` FROM `foobar`', $query['sql']); + $this->assertEquals([], $query['params']); + } + + /** + * + */ + public function testVectorInsertValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->getCompiler()->insert(['embedding' => new InVector([1, 2, 3])]); + + $this->assertEquals('INSERT INTO `foobar` (`embedding`) VALUES (VEC_FromText(?))', $query['sql']); + $this->assertEquals(['[1,2,3]'], $query['params']); + } + + /** + * + */ + public function testStringVectorInsertValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->getCompiler()->insert(['embedding' => new InVector('[1,2,3]')]); + + $this->assertEquals('INSERT INTO `foobar` (`embedding`) VALUES (VEC_FromText(?))', $query['sql']); + $this->assertEquals(['[1,2,3]'], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/MySQLCompilerTest.php b/tests/unit/database/query/compilers/MySQLCompilerTest.php index 1b5e9c12e..7852f0744 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -12,6 +12,8 @@ use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; use mako\database\query\Subquery; +use mako\database\query\values\in\Vector as InVector; +use mako\database\query\values\out\Vector as OutVector; use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; @@ -388,6 +390,64 @@ public function testDescendingVectorDistance(): void $this->assertEquals(['[1,2,3,4,5]'], $query['params']); } + /** + * + */ + public function testVectorSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVector('embedding')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VECTOR_TO_STRING(`embedding`) FROM `foobar`', $query['sql']); + $this->assertEquals([], $query['params']); + } + + /** + * + */ + public function testVectorSelectValueWithAlias(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVector('embedding')->as('vector')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VECTOR_TO_STRING(`embedding`) AS `vector` FROM `foobar`', $query['sql']); + $this->assertEquals([], $query['params']); + } + + /** + * + */ + public function testVectorInsertValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->getCompiler()->insert(['embedding' => new InVector([1, 2, 3])]); + + $this->assertEquals('INSERT INTO `foobar` (`embedding`) VALUES (STRING_TO_VECTOR(?))', $query['sql']); + $this->assertEquals(['[1,2,3]'], $query['params']); + } + + /** + * + */ + public function testStringVectorInsertValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->getCompiler()->insert(['embedding' => new InVector('[1,2,3]')]); + + $this->assertEquals('INSERT INTO `foobar` (`embedding`) VALUES (STRING_TO_VECTOR(?))', $query['sql']); + $this->assertEquals(['[1,2,3]'], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/PostgresCompilerTest.php b/tests/unit/database/query/compilers/PostgresCompilerTest.php index 15b999d5f..55487a552 100644 --- a/tests/unit/database/query/compilers/PostgresCompilerTest.php +++ b/tests/unit/database/query/compilers/PostgresCompilerTest.php @@ -12,6 +12,8 @@ use mako\database\query\helpers\HelperInterface; use mako\database\query\Query; use mako\database\query\Subquery; +use mako\database\query\values\in\Vector as InVector; +use mako\database\query\values\out\Vector as OutVector; use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; @@ -360,6 +362,64 @@ public function testDescendingVectorDistance(): void $this->assertEquals(['[1,2,3,4,5]'], $query['params']); } + /** + * + */ + public function testVectorSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVector('embedding')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT "embedding" FROM "foobar"', $query['sql']); + $this->assertEquals([], $query['params']); + } + + /** + * + */ + public function testVectorSelectValueWithAlias(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVector('embedding')->as('vector')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT "embedding" AS "vector" FROM "foobar"', $query['sql']); + $this->assertEquals([], $query['params']); + } + + /** + * + */ + public function testVectorInsertValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->getCompiler()->insert(['embedding' => new InVector([1, 2, 3])]); + + $this->assertEquals('INSERT INTO "foobar" ("embedding") VALUES (?)', $query['sql']); + $this->assertEquals(['[1,2,3]'], $query['params']); + } + + /** + * + */ + public function testStringVectorInsertValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->getCompiler()->insert(['embedding' => new InVector('[1,2,3]')]); + + $this->assertEquals('INSERT INTO "foobar" ("embedding") VALUES (?)', $query['sql']); + $this->assertEquals(['[1,2,3]'], $query['params']); + } + /** * */ From 4b50983a7718e586c4c096c1179018bba7087925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Tue, 10 Feb 2026 19:46:17 +0100 Subject: [PATCH 11/14] Made it easier to retrieve vector distances --- .../query/values/out/VectorDistance.php | 96 +++++++++++++++++++ .../query/compilers/MariaDBCompilerTest.php | 46 +++++++++ .../query/compilers/MySQLCompilerTest.php | 46 +++++++++ .../query/compilers/PostgresCompilerTest.php | 46 +++++++++ 4 files changed, 234 insertions(+) create mode 100644 src/mako/database/query/values/out/VectorDistance.php diff --git a/src/mako/database/query/values/out/VectorDistance.php b/src/mako/database/query/values/out/VectorDistance.php new file mode 100644 index 000000000..97d9c8ff4 --- /dev/null +++ b/src/mako/database/query/values/out/VectorDistance.php @@ -0,0 +1,96 @@ +vectorDistance) { + VectorDistanceType::COSINE => 'VEC_DISTANCE_COSINE', + VectorDistanceType::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', + }; + + return "{$function}({$compiler->escapeColumnName($this->column)}, VEC_FromText(?))"; + } + + /** + * Gets the MySQL distance SQL. + */ + protected function getMySqlDistance(Compiler $compiler): string + { + $function = match ($this->vectorDistance) { + VectorDistanceType::COSINE => 'COSINE', + VectorDistanceType::EUCLIDEAN => 'EUCLIDEAN', + }; + + return "DISTANCE({$compiler->escapeColumnName($this->column)}, STRING_TO_VECTOR(?), '{$function}')"; + } + + /** + * Gets the Postgres distance SQL. + */ + protected function getPostgresDistance(Compiler $compiler): string + { + $function = match ($this->vectorDistance) { + VectorDistanceType::COSINE => '<=>', + VectorDistanceType::EUCLIDEAN => '<->', + }; + + return "{$compiler->columnName($this->column)} {$function} ?"; + } + + /** + * {@inheritDoc} + */ + #[Override] + public function getSql(Compiler $compiler): string + { + return match ($compiler::class) { + MariaDB::class => $this->getMariaDbDistance($compiler), + MySQL::class => $this->getMySqlDistance($compiler), + Postgres::class => $this->getPostgresDistance($compiler), + default => throw new DatabaseException(sprintf('Vector values are not supported by the [ %s ] compiler.', $compiler::class)), + }; + } + + /** + * {@inheritDoc} + */ + #[Override] + public function getParameters(): ?array + { + return [ + is_array($this->vector) ? json_encode($this->vector) : $this->vector, + ]; + } +} diff --git a/tests/unit/database/query/compilers/MariaDBCompilerTest.php b/tests/unit/database/query/compilers/MariaDBCompilerTest.php index beef84bb3..5778d5c49 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -14,6 +14,7 @@ use mako\database\query\Subquery; use mako\database\query\values\in\Vector as InVector; use mako\database\query\values\out\Vector as OutVector; +use mako\database\query\values\out\VectorDistance as OutVectorDistance; use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; @@ -231,6 +232,51 @@ public function testVectorSelectValueWithAlias(): void $this->assertEquals([], $query['params']); } + /** + * + */ + public function testVectorCosineDistanceSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4])]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) FROM `foobar`', $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + + /** + * + */ + public function testVectorEuclideanDistanceSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4], VectorDistance::EUCLIDEAN)]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VEC_DISTANCE_EUCLIDEAN(`embedding`, VEC_FromText(?)) FROM `foobar`', $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + + /** + * + */ + public function testVectorDistanceSelectValueWithAlias(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4])->as('distance')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) AS `distance` FROM `foobar`', $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/MySQLCompilerTest.php b/tests/unit/database/query/compilers/MySQLCompilerTest.php index 7852f0744..355e695e9 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -14,6 +14,7 @@ use mako\database\query\Subquery; use mako\database\query\values\in\Vector as InVector; use mako\database\query\values\out\Vector as OutVector; +use mako\database\query\values\out\VectorDistance as OutVectorDistance; use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; @@ -420,6 +421,51 @@ public function testVectorSelectValueWithAlias(): void $this->assertEquals([], $query['params']); } + /** + * + */ + public function testVectorCosineDistanceSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4])]) + ->getCompiler()->select(); + + $this->assertEquals("SELECT DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') FROM `foobar`", $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + + /** + * + */ + public function testVectorEuclideanDistanceSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4], VectorDistance::EUCLIDEAN)]) + ->getCompiler()->select(); + + $this->assertEquals("SELECT DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'EUCLIDEAN') FROM `foobar`", $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + + /** + * + */ + public function testVectorDistanceSelectValueWithAlias(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4])->as('distance')]) + ->getCompiler()->select(); + + $this->assertEquals("SELECT DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') AS `distance` FROM `foobar`", $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + /** * */ diff --git a/tests/unit/database/query/compilers/PostgresCompilerTest.php b/tests/unit/database/query/compilers/PostgresCompilerTest.php index 55487a552..18e290744 100644 --- a/tests/unit/database/query/compilers/PostgresCompilerTest.php +++ b/tests/unit/database/query/compilers/PostgresCompilerTest.php @@ -14,6 +14,7 @@ use mako\database\query\Subquery; use mako\database\query\values\in\Vector as InVector; use mako\database\query\values\out\Vector as OutVector; +use mako\database\query\values\out\VectorDistance as OutVectorDistance; use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; @@ -392,6 +393,51 @@ public function testVectorSelectValueWithAlias(): void $this->assertEquals([], $query['params']); } + /** + * + */ + public function testVectorCosineDistanceSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4])]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT "embedding" <=> ? FROM "foobar"', $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + + /** + * + */ + public function testVectorEuclideanDistanceSelectValue(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4], VectorDistance::EUCLIDEAN)]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT "embedding" <-> ? FROM "foobar"', $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + + /** + * + */ + public function testVectorDistanceSelectValueWithAlias(): void + { + $query = $this->getBuilder(); + + $query = $query->table('foobar') + ->select([new OutVectorDistance('embedding', [1, 2, 3, 4])->as('distance')]) + ->getCompiler()->select(); + + $this->assertEquals('SELECT "embedding" <=> ? AS "distance" FROM "foobar"', $query['sql']); + $this->assertEquals(['[1,2,3,4]'], $query['params']); + } + /** * */ From 8dfb46ad825f7b665d3fc48ddcf3146fd590458a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Tue, 10 Feb 2026 19:57:28 +0100 Subject: [PATCH 12/14] Cleaning up --- src/mako/database/query/values/out/Value.php | 11 +++++++++++ src/mako/database/query/values/out/Vector.php | 9 --------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/mako/database/query/values/out/Value.php b/src/mako/database/query/values/out/Value.php index db71a238e..2b6d6563b 100644 --- a/src/mako/database/query/values/out/Value.php +++ b/src/mako/database/query/values/out/Value.php @@ -7,6 +7,8 @@ namespace mako\database\query\values\out; +use Override; + /** * Base value. */ @@ -14,6 +16,15 @@ abstract class Value implements ValueWithAliasInterface { protected ?string $alias = null; + /** + * {@inheritDoc} + */ + #[Override] + public function getParameters(): ?array + { + return null; + } + /** * Sets the value alias. * diff --git a/src/mako/database/query/values/out/Vector.php b/src/mako/database/query/values/out/Vector.php index ce4599658..aa8bc0175 100644 --- a/src/mako/database/query/values/out/Vector.php +++ b/src/mako/database/query/values/out/Vector.php @@ -38,13 +38,4 @@ public function getSql(Compiler $compiler): string default => throw new DatabaseException(sprintf('Vector values are not supported by the [ %s ] compiler.', $compiler::class)), }; } - - /** - * {@inheritDoc} - */ - #[Override] - public function getParameters(): ?array - { - return null; - } } From 66399a9543f24b989ca7d064aacff4d61602fea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Tue, 10 Feb 2026 20:00:51 +0100 Subject: [PATCH 13/14] Update core.php --- src/mako/application/cli/commands/app/preloader/core.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mako/application/cli/commands/app/preloader/core.php b/src/mako/application/cli/commands/app/preloader/core.php index 8fe8cec00..a6e3ba784 100644 --- a/src/mako/application/cli/commands/app/preloader/core.php +++ b/src/mako/application/cli/commands/app/preloader/core.php @@ -78,6 +78,12 @@ mako\database\query\Result::class, mako\database\query\ResultSet::class, mako\database\query\Subquery::class, + mako\database\query\values\in\Vector::class, + mako\database\query\values\out\Value::class, + mako\database\query\values\out\ValueWithAliasInterface::class, + mako\database\query\values\out\Vector::class, + mako\database\query\values\out\VectorDistance::class, + mako\database\query\values\ValueInterface::class, mako\database\query\VectorDistance::class, mako\error\ErrorHandler::class, mako\file\FileSystem::class, From 93b8fb2a0d9b0f8cde0f768d610af81aad858ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Tue, 10 Feb 2026 20:03:00 +0100 Subject: [PATCH 14/14] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a10a46331..93bb4d293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `MariaDB` - `MySQL` - `Postgres` +* Now possible to define custom input/output value objects for the query builder. --------------------------------------------------------