diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfca383c..93bb4d293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### 12.1.0 (2026-??-??) + +#### New + +* Added vector query support to the query builder for the following databases: + - `MariaDB` + - `MySQL` + - `Postgres` +* Now possible to define custom input/output value objects for the query builder. + +-------------------------------------------------------- + ### 11.4.6, 12.0.2 (2026-01-08) 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; } diff --git a/src/mako/application/cli/commands/app/preloader/core.php b/src/mako/application/cli/commands/app/preloader/core.php index cd6282cd9..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,13 @@ 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, mako\file\Permission::class, diff --git a/src/mako/database/query/Query.php b/src/mako/database/query/Query.php index 4cbaa76e5..87dcf6833 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 distance clause. + * + * @return $this + */ + 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, + 'maxDistance' => $maxDistance, + 'vectorDistance' => $vectorDistance, + 'separator' => $separator, + ]; + + return $this; + } + + /** + * Adds a vector distance clause. + * + * @return $this + */ + public function orWhereVectorDistance(string $column, array|string|Subquery $vector, float $maxDistance = 0.2, VectorDistance $vectorDistance = VectorDistance::COSINE): static + { + return $this->whereVectorDistance($column, $vector, $maxDistance, $vectorDistance, 'OR'); + } + /** * Adds a BETWEEN clause. * @@ -1040,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', ]; @@ -1097,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/VectorDistance.php b/src/mako/database/query/VectorDistance.php new file mode 100644 index 000000000..52a23b6dd --- /dev/null +++ b/src/mako/database/query/VectorDistance.php @@ -0,0 +1,17 @@ +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; @@ -483,6 +518,14 @@ protected function whereColumn(array $where): string return "{$this->columnName($where['column1'])} {$where['operator']} {$this->columnName($where['column2'])}"; } + /** + * Compiles vector distance clauses. + */ + protected function whereVectorDistance(array $where): string + { + throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector distance calculations.', static::class)); + } + /** * Compiles BETWEEN clauses. */ @@ -631,6 +674,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. */ @@ -643,7 +702,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 ff6d6a214..bc9074f54 100644 --- a/src/mako/database/query/compilers/MariaDB.php +++ b/src/mako/database/query/compilers/MariaDB.php @@ -7,13 +7,62 @@ namespace mako\database\query\compilers; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use Override; +use function is_array; +use function json_encode; + /** * Compiles MariaDB queries. */ class MariaDB extends MySQL { + /** + * Returns a vector distance calculation. + */ + protected function vectorDistance(array $vectorDistance): string + { + $vector = $vectorDistance['vector']; + + 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 ($vectorDistance['vectorDistance']) { + VectorDistance::COSINE => 'VEC_DISTANCE_COSINE', + VectorDistance::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN', + }; + + return "{$function}({$this->column($vectorDistance['column'], false)}, {$vector})"; + } + + /** + * {@inheritDoc} + */ + #[Override] + 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 b142cf595..8cc81fde5 100644 --- a/src/mako/database/query/compilers/MySQL.php +++ b/src/mako/database/query/compilers/MySQL.php @@ -8,8 +8,12 @@ namespace mako\database\query\compilers; use mako\database\query\compilers\traits\JsonPathBuilderTrait; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; use Override; +use function is_array; +use function json_encode; use function str_replace; /** @@ -52,6 +56,50 @@ protected function buildJsonSet(string $column, array $segments, string $param): return "{$column} = JSON_SET({$column}, '{$this->buildJsonPath($segments)}', CAST({$param} AS JSON))"; } + /** + * Returns a vector distance calculation. + */ + protected function vectorDistance(array $vectorDistance): string + { + $vector = $vectorDistance['vector']; + + if ($vector instanceof Subquery) { + $vector = $this->subquery($vector); + } + else { + if (is_array($vector)) { + $vector = json_encode($vector); + } + + $vector = "STRING_TO_VECTOR({$this->param($vector)})"; + } + + $function = match ($vectorDistance['vectorDistance']) { + VectorDistance::COSINE => 'COSINE', + VectorDistance::EUCLIDEAN => 'EUCLIDEAN', + }; + + 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'])}"; + } + + /** + * {@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 1d5717eac..39e73b51b 100644 --- a/src/mako/database/query/compilers/Postgres.php +++ b/src/mako/database/query/compilers/Postgres.php @@ -7,11 +7,15 @@ namespace mako\database\query\compilers; +use mako\database\query\Subquery; +use mako\database\query\VectorDistance; 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 +59,50 @@ protected function buildJsonSet(string $column, array $segments, string $param): return $column . " = JSONB_SET({$column}, '{" . str_replace("'", "''", implode(',', $segments)) . "}', '{$param}')"; } + /** + * Returns a vector distance calculation. + */ + protected function vectorDistance(array $vectorDistance): string + { + $vector = $vectorDistance['vector']; + + if ($vector instanceof Subquery) { + $vector = $this->subquery($vector); + } + else { + if (is_array($vector)) { + $vector = json_encode($vector); + } + + $vector = $this->param($vector); + } + + $function = match ($vectorDistance['vectorDistance']) { + VectorDistance::COSINE => '<=>', + VectorDistance::EUCLIDEAN => '<->', + }; + + return "{$this->column($vectorDistance['column'], false)} {$function} {$vector}"; + } + + /** + * {@inheritDoc} + */ + #[Override] + 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/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..2b6d6563b --- /dev/null +++ b/src/mako/database/query/values/out/Value.php @@ -0,0 +1,47 @@ +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)), + }; + } +} 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/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 5cdbe7766..5778d5c49 100644 --- a/tests/unit/database/query/compilers/MariaDBCompilerTest.php +++ b/tests/unit/database/query/compilers/MariaDBCompilerTest.php @@ -11,6 +11,11 @@ use mako\database\query\compilers\MariaDB as MariaDBCompiler; 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\values\out\VectorDistance as OutVectorDistance; +use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -43,6 +48,263 @@ protected function getBuilder($table = 'foobar') return (new Query($this->getConnection()))->table($table); } + /** + * + */ + 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 VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) <= ?', $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, 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']); + } + + /** + * + */ + 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']); + } + + /** + * + */ + 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 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']); + } + + /** + * + */ + 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 35a14ad41..355e695e9 100644 --- a/tests/unit/database/query/compilers/MySQLCompilerTest.php +++ b/tests/unit/database/query/compilers/MySQLCompilerTest.php @@ -11,6 +11,11 @@ use mako\database\query\compilers\MySQL as MySQLCompiler; 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\values\out\VectorDistance as OutVectorDistance; +use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -232,6 +237,263 @@ public function testBasicInsertWithNoValues(): void $this->assertEquals([], $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 DISTANCE(`embedding`, STRING_TO_VECTOR(?), 'COSINE') <= ?", $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, 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']); + } + + /** + * + */ + 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']); + } + + /** + * + */ + 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 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']); + } + + /** + * + */ + 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 bccb430bd..18e290744 100644 --- a/tests/unit/database/query/compilers/PostgresCompilerTest.php +++ b/tests/unit/database/query/compilers/PostgresCompilerTest.php @@ -11,6 +11,11 @@ use mako\database\query\compilers\Postgres as PostgresCompiler; 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\values\out\VectorDistance as OutVectorDistance; +use mako\database\query\VectorDistance; use mako\tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -204,6 +209,263 @@ 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, 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']); + } + + /** + * + */ + 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']); + } + + /** + * + */ + 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 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']); + } + + /** + * + */ + 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']); + } + /** * */