Skip to content

Commit f790ba7

Browse files
committed
fix(postgres): write ?| inline instead of through a function
PDO passes the escaped ??| through under emulated prepares as long as no named placeholder repeats in the statement. The cursor equality conditions now bind per branch, and the projected vector distance binds its own copy of the vector, so the permission condition can use ?| directly. That removes the function and its creation in create().
1 parent 833dce6 commit f790ba7

2 files changed

Lines changed: 13 additions & 44 deletions

File tree

‎src/Database/Adapter/Postgres.php‎

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ class Postgres extends SQL
3232
{
3333
public const MAX_IDENTIFIER_NAME = 63;
3434

35-
/**
36-
* Function that tests a row's permissions against a set of roles with jsonb's ?| operator.
37-
*/
38-
protected const PERMISSIONS_FUNCTION = '_permissions_any';
39-
4035
/**
4136
* @inheritDoc
4237
*/
@@ -123,9 +118,6 @@ public function create(string $name): bool
123118
$name = $this->filter($name);
124119

125120
if ($this->exists($name)) {
126-
// Schemas created before the function existed get it here.
127-
$this->createPermissionsFunction($name);
128-
129121
return true;
130122
}
131123

@@ -149,9 +141,6 @@ public function create(string $name): bool
149141
)
150142
";
151143
$this->getPDO()->prepare($collation)->execute();
152-
153-
$this->createPermissionsFunction($name);
154-
155144
return $dbCreation;
156145
}
157146

@@ -1842,45 +1831,23 @@ protected function getSQLPermissionsCondition(
18421831
throw new DatabaseException('Unknown permission type: ' . $type);
18431832
}
18441833

1845-
if ($roles === []) {
1846-
return 'FALSE';
1847-
}
1848-
18491834
$column = "{$this->quote($alias)}.{$this->quote('_permissions')}";
18501835

1851-
// One ?| for all roles keeps the estimate flat however many roles there are; a @> per
1852-
// role adds up until the planner gives up on the GIN index. The operator goes through
1853-
// the inlined function because PDO reads a lone ? as a positional placeholder, and
1854-
// doubling it to escape breaks once a named placeholder is repeated, which the cursor
1855-
// conditions do. jsonb_exists_any would avoid both but is not indexable.
1836+
// One ?| for all roles; a @> per role adds to the row estimate until the planner gives
1837+
// up on the GIN index. ?? is PDO's escape for a literal ?, which emulated prepares only
1838+
// accept while no named placeholder appears twice in the statement, so the queries this
1839+
// condition joins bind each value under its own name. jsonb_exists_any would avoid the
1840+
// ? but is not indexable.
18561841
$permissions = \array_map(
18571842
fn ($role) => $this->getPDO()->quote("{$type}(\"{$role}\")"),
18581843
$roles
18591844
);
18601845

1861-
return "{$this->getSQLSchema()}{$this->quote(self::PERMISSIONS_FUNCTION)}({$column}, ARRAY[" . \implode(', ', $permissions) . ']::text[])';
1862-
}
1846+
if ($permissions === []) {
1847+
return 'FALSE';
1848+
}
18631849

1864-
/**
1865-
* Create the function permission checks are written against, unless the schema has it.
1866-
*
1867-
* @param string $schema
1868-
* @return void
1869-
*/
1870-
protected function createPermissionsFunction(string $schema): void
1871-
{
1872-
$function = "\"{$schema}\".\"" . self::PERMISSIONS_FUNCTION . '"';
1873-
1874-
// Run through exec, which does not look for placeholders. A concurrent creation of
1875-
// the same function is not an error.
1876-
$this->getPDO()->exec("
1877-
DO \$\$ BEGIN
1878-
CREATE FUNCTION {$function}(jsonb, text[]) RETURNS boolean
1879-
LANGUAGE sql IMMUTABLE PARALLEL SAFE
1880-
AS 'SELECT \$1 OPERATOR(pg_catalog.?|) \$2';
1881-
EXCEPTION WHEN duplicate_function OR unique_violation THEN NULL;
1882-
END \$\$
1883-
");
1850+
return "{$column} ??| ARRAY[" . \implode(', ', $permissions) . ']::text[]';
18841851
}
18851852

18861853
/**

‎src/Database/Adapter/SQL.php‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,7 +3047,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
30473047
$prevOriginal = $orderAttributes[$j];
30483048
$prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal));
30493049

3050-
$bindName = ":cursor_{$j}";
3050+
$bindName = ":cursor_{$i}_{$j}";
30513051
$binds[$bindName] = $cursor[$prevOriginal];
30523052

30533053
$conditions[] = "{$this->quote($alias)}.{$this->quote($prevAttr)} = {$bindName}";
@@ -3118,7 +3118,9 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
31183118
$projection = $this->getAttributeProjection($selections, $alias);
31193119

31203120
if (!empty($vectorDistances)) {
3121-
$readable = $this->getSQLReadableDistance($vectorDistances[0]);
3121+
// Built again so its vector binds under a name the ORDER BY doesn't use.
3122+
$distance = $this->getSQLVectorDistance($vectorQueries[0], $binds, $alias) ?? $vectorDistances[0];
3123+
$readable = $this->getSQLReadableDistance($distance);
31223124
$projection .= ", {$readable} AS {$this->quote(static::VECTOR_DISTANCE_COLUMN)}";
31233125
}
31243126

0 commit comments

Comments
 (0)