Skip to content

Commit 833dce6

Browse files
committed
fix(postgres): create the permissions function with the database
create() now adds the function whether or not the schema already exists, so databases created before it get it the next time create() runs. Permission conditions always call the function, which drops the per-connection existence check, its cache and the fallback to one containment check per role.
1 parent b2cb5ec commit 833dce6

1 file changed

Lines changed: 19 additions & 68 deletions

File tree

‎src/Database/Adapter/Postgres.php‎

Lines changed: 19 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,6 @@ class Postgres extends SQL
3737
*/
3838
protected const PERMISSIONS_FUNCTION = '_permissions_any';
3939

40-
/**
41-
* Schemas known to have the permissions function, per connection.
42-
*
43-
* @var \WeakMap<object, array<string, bool>>|null
44-
*/
45-
protected static ?\WeakMap $permissionsFunctions = null;
46-
4740
/**
4841
* @inheritDoc
4942
*/
@@ -130,6 +123,9 @@ public function create(string $name): bool
130123
$name = $this->filter($name);
131124

132125
if ($this->exists($name)) {
126+
// Schemas created before the function existed get it here.
127+
$this->createPermissionsFunction($name);
128+
133129
return true;
134130
}
135131

@@ -154,7 +150,7 @@ public function create(string $name): bool
154150
";
155151
$this->getPDO()->prepare($collation)->execute();
156152

157-
$this->ensurePermissionsFunction($name);
153+
$this->createPermissionsFunction($name);
158154

159155
return $dbCreation;
160156
}
@@ -174,10 +170,6 @@ public function delete(string $name): bool
174170
$sql = "DROP SCHEMA IF EXISTS \"{$name}\" CASCADE";
175171
$sql = $this->trigger(Database::EVENT_DATABASE_DELETE, $sql);
176172

177-
if (self::$permissionsFunctions !== null) {
178-
unset(self::$permissionsFunctions[$this->getPDO()]);
179-
}
180-
181173
return $this->getPDO()->prepare($sql)->execute();
182174
}
183175

@@ -1861,75 +1853,34 @@ protected function getSQLPermissionsCondition(
18611853
// the inlined function because PDO reads a lone ? as a positional placeholder, and
18621854
// doubling it to escape breaks once a named placeholder is repeated, which the cursor
18631855
// conditions do. jsonb_exists_any would avoid both but is not indexable.
1864-
if ($this->ensurePermissionsFunction($this->getDatabase())) {
1865-
$permissions = \array_map(
1866-
fn ($role) => $this->getPDO()->quote("{$type}(\"{$role}\")"),
1867-
$roles
1868-
);
1869-
1870-
return "{$this->getSQLSchema()}{$this->quote(self::PERMISSIONS_FUNCTION)}({$column}, ARRAY[" . \implode(', ', $permissions) . ']::text[])';
1871-
}
1872-
18731856
$permissions = \array_map(
1874-
fn ($role) => "{$column} @> {$this->getPDO()->quote(\json_encode(["{$type}(\"{$role}\")"]))}::jsonb",
1857+
fn ($role) => $this->getPDO()->quote("{$type}(\"{$role}\")"),
18751858
$roles
18761859
);
18771860

1878-
return '(' . \implode(' OR ', $permissions) . ')';
1861+
return "{$this->getSQLSchema()}{$this->quote(self::PERMISSIONS_FUNCTION)}({$column}, ARRAY[" . \implode(', ', $permissions) . ']::text[])';
18791862
}
18801863

18811864
/**
1882-
* Make sure the schema has the function permission checks are written against.
1883-
*
1884-
* Databases created before it existed get it on first use. Where it can't be created
1885-
* (inside a transaction that may yet roll back, or without the rights to), the caller
1886-
* falls back to one containment check per role.
1865+
* Create the function permission checks are written against, unless the schema has it.
18871866
*
18881867
* @param string $schema
1889-
* @return bool
1868+
* @return void
18901869
*/
1891-
protected function ensurePermissionsFunction(string $schema): bool
1870+
protected function createPermissionsFunction(string $schema): void
18921871
{
1893-
$pdo = $this->getPDO();
1894-
$schema = $this->filter($schema);
1895-
1896-
$known = self::$permissionsFunctions ??= new \WeakMap();
1897-
1898-
if (isset($known[$pdo][$schema])) {
1899-
return true;
1900-
}
1901-
19021872
$function = "\"{$schema}\".\"" . self::PERMISSIONS_FUNCTION . '"';
19031873

1904-
try {
1905-
$stmt = $pdo->prepare('SELECT to_regprocedure(:function) IS NOT NULL');
1906-
$stmt->bindValue(':function', "{$function}(jsonb, text[])");
1907-
$stmt->execute();
1908-
$exists = (bool) $stmt->fetchColumn();
1909-
1910-
if (!$exists) {
1911-
if ($pdo->inTransaction()) {
1912-
return false;
1913-
}
1914-
1915-
// Run through exec, which does not look for placeholders. A concurrent
1916-
// creation of the same function is not an error.
1917-
$pdo->exec("
1918-
DO \$\$ BEGIN
1919-
CREATE FUNCTION {$function}(jsonb, text[]) RETURNS boolean
1920-
LANGUAGE sql IMMUTABLE PARALLEL SAFE
1921-
AS 'SELECT \$1 OPERATOR(pg_catalog.?|) \$2';
1922-
EXCEPTION WHEN duplicate_function OR unique_violation THEN NULL;
1923-
END \$\$
1924-
");
1925-
}
1926-
} catch (PDOException) {
1927-
return false;
1928-
}
1929-
1930-
$known[$pdo] = [...($known[$pdo] ?? []), $schema => true];
1931-
1932-
return true;
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+
");
19331884
}
19341885

19351886
/**

0 commit comments

Comments
 (0)