Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class Postgres extends SQL implements Feature\Spatial, Feature\Timeouts

public const int MAX_IDENTIFIER_NAME = 63;

protected const int MAX_LABEL_LENGTH = self::MAX_IDENTIFIER_NAME;

protected const string MIN_DATETIME = '-4713-01-01 00:00:00';

private const string QUOTED_IDENTIFIER = '/["\x{AB}\x{BB}\x{201C}\x{201D}\x{201E}\x{300C}\x{300D}][\s\x{A0}\x{202F}]*([^"\x{AB}\x{BB}\x{201C}\x{201D}\x{201E}\x{300C}\x{300D}]+?)[\s\x{A0}\x{202F}]*["\x{AB}\x{BB}\x{201C}\x{201D}\x{201E}\x{300C}\x{300D}]/u';
Expand Down
51 changes: 45 additions & 6 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ abstract class SQL extends Adapter implements Feature\Connection, Feature\RawQue

private const string FOJ_ROWS_ALIAS = 'foj_rows';

/**
* A joined column whose `alias.column` label is longer than the engine keeps is labelled with
* this prefix and a digest of the label instead, and remapRow() reads it back. A join alias
* cannot hold `$`.
*/
private const string JOIN_LABEL_PREFIX = '$join_';

/**
* The `alias.column` label each digest label stands for. A digest names one label for good, so
* the map only grows with the joined columns a process reads.
*
* @var array<string, string>
*/
private static array $joinLabels = [];

/**
* MariaDB and MySQL keep a column label of up to 256 characters.
*/
protected const int MAX_LABEL_LENGTH = 256;

protected const string MIN_DATETIME = '1000-01-01 00:00:00';

/**
Expand Down Expand Up @@ -2109,7 +2129,7 @@ private function rowsColumn(string $attribute, array $queries, bool $grouped): s
return $this->qualifiedGroupNames($values)[$index] ?? $column;
}

return $dot === false ? $column : \substr($attribute, 0, $dot).'.'.$column;
return $dot === false ? $column : $this->joinLabel(\substr($attribute, 0, $dot), $column);
}

throw new QueryException('Invalid query: sum() over '.($grouped ? 'grouped' : 'distinct').' rows adds up one of their '.($grouped ? 'groupBy' : 'selected').' attributes, and '.$attribute.' is not one of them');
Expand Down Expand Up @@ -4194,7 +4214,6 @@ protected function mapSelectionsToColumns(array $selections, bool $includeIntern
}

$aliasSet = \array_fill_keys($joinAliases, true);
$quote = $this->getIdentifierQuote();
$columns = [];
foreach ($selections as $selection) {
$dot = \strpos($selection, '.');
Expand All @@ -4203,9 +4222,7 @@ protected function mapSelectionsToColumns(array $selections, bool $includeIntern
if (isset($aliasSet[$prefix])) {
$name = \substr($selection, $dot + 1);
$internal = $this->filter($this->getInternalKeyForAttribute($name));
$qualified = $quote.$this->filter($prefix).$quote.'.'.$quote.$internal.$quote;
$output = $prefix.'.'.$internal;
$columns[] = $qualified.' AS '.$quote.$output.$quote;
$columns[] = $this->quote($prefix).'.'.$this->quote($internal).' AS '.$this->quote($this->joinLabel($prefix, $internal));

continue;
}
Expand All @@ -4216,6 +4233,23 @@ protected function mapSelectionsToColumns(array $selections, bool $includeIntern
return $columns;
}

/**
* The label a joined column is read under: `alias.column`, or a digest of it when that is longer
* than the engine keeps a label, so the engine cannot cut it short.
*/
private function joinLabel(string $alias, string $column): string
{
$label = $alias.'.'.$column;
if (\strlen($label) <= static::MAX_LABEL_LENGTH) {
return $label;
}

$digest = self::JOIN_LABEL_PREFIX.\md5($label);
self::$joinLabels[$digest] = $label;

return $digest;
}

/**
* The projection of a join without a select or with `*`: every column of the main table, and under each
* join alias the joined collection's `$id` and the attributes the Database layer handed over for it. A
Expand Down Expand Up @@ -6401,7 +6435,7 @@ private function qualifyOrderAttribute(string $attribute, array $joinAliases = [
}

/**
* @param array<int|string, mixed> $row
* @param array<int|string, mixed> $row
*/
private function remapRow(array &$row): void
{
Expand All @@ -6410,6 +6444,11 @@ private function remapRow(array &$row): void
unset($row[$key]);
continue;
}
if (isset(self::$joinLabels[$key])) {
$row[self::$joinLabels[$key]] = $row[$key];
unset($row[$key]);
$key = self::$joinLabels[$key];
}
if (\str_starts_with($key, self::FOJ_ORDER_ALIAS_PREFIX)) {
unset($row[$key]);

Expand Down
6 changes: 5 additions & 1 deletion src/Database/Adapter/SQL/Hook/Permission/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ public function __construct(
protected string $quoteCharacter = '`',
protected bool $semiJoin = true,
) {
foreach ([$documentColumn, $permissionDocumentColumn, $permissionRoleColumn, $permissionTypeColumn, $scopeColumn] as $column) {
if (! \preg_match(self::QUOTED_IDENTIFIER_PATTERN, $documentColumn)) {
throw new InvalidArgumentException('Invalid column name: '.$documentColumn);
}

foreach ([$permissionDocumentColumn, $permissionRoleColumn, $permissionTypeColumn, $scopeColumn] as $column) {
if (! \preg_match(self::IDENTIFIER_PATTERN, $column)) {
throw new InvalidArgumentException('Invalid column name: '.$column);
}
Expand Down
20 changes: 17 additions & 3 deletions src/Database/Validator/Query/Join.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
*/
class Join extends Base
{
public const string ALIAS_PATTERN = '/^[A-Za-z_][A-Za-z0-9_]*$/';
/**
* The characters of an Appwrite ID but `.`, which separates an alias from its column.
*/
public const string ALIAS_PATTERN = '/^[A-Za-z0-9_][A-Za-z0-9_-]*$/';

/**
* PostgreSQL folds an identifier past 63 bytes, so two longer aliases could read as one.
*/
public const int MAX_ALIAS_LENGTH = 63;

private const array OPERATORS = ['=', '!=', '<', '>', '<=', '>=', '<>'];

Expand Down Expand Up @@ -133,8 +141,14 @@ public static function describeInvalidAlias(string $alias, bool $defaulted = fal

if (\preg_match(self::ALIAS_PATTERN, $alias) !== 1) {
return $defaulted
? self::describeUnusableTable($alias, 'an alias must start with a letter or an underscore and contain only letters, digits and underscores')
: 'Join alias must start with a letter or an underscore and contain only letters, digits and underscores';
? self::describeUnusableTable($alias, 'an alias must start with a letter, a digit or an underscore and contain only letters, digits, underscores and hyphens')
: 'Join alias must start with a letter, a digit or an underscore and contain only letters, digits, underscores and hyphens';
}

if (\strlen($alias) > self::MAX_ALIAS_LENGTH) {
return $defaulted
? self::describeUnusableTable($alias, 'an alias must be at most '.self::MAX_ALIAS_LENGTH.' characters')
: 'Join alias must be at most '.self::MAX_ALIAS_LENGTH.' characters';
}

if (\strcasecmp($alias, Query::DEFAULT_ALIAS) === 0) {
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/Adapter/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Tests\E2E\Adapter\Scopes\GeneralTests;
use Tests\E2E\Adapter\Scopes\IndexTests;
use Tests\E2E\Adapter\Scopes\IntegerRoundingTests;
use Tests\E2E\Adapter\Scopes\JoinAliasIdTests;
use Tests\E2E\Adapter\Scopes\JoinAliasTests;
use Tests\E2E\Adapter\Scopes\JoinComboTests;
use Tests\E2E\Adapter\Scopes\JoinReadRuleTests;
Expand Down Expand Up @@ -59,6 +60,7 @@ abstract class Base extends TestCase
use GeneralTests;
use IndexTests;
use IntegerRoundingTests;
use JoinAliasIdTests;
use JoinAliasTests;
use JoinComboTests;
use JoinReadRuleTests;
Expand Down
Loading
Loading