Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 58
Joins#467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Joins #467
Changes from all commits
13beed1966631b8004014c7af565a87eed393d414e35f73e6fc83d9bc5c8010f4bddd49c85f4ef99eba73c6101f6276e3be7a56b5558820b778d8da4a1c539f2007073ca3335ab49c0dc44bee57bc1fe9608e5eca0ca81dad0c884601db102de99c0c2a4ba1ba657456bde5abf41ee3567f1bbb1fff4425a21d100d0f591e38cfcc71a45b18c882dca965c71026ec5ea7f9a2d22c108d8caa65f93e7265bd4387a785e0c1bf293fc1f1ea34ccbb984f807737651a24c7d0fe4f55e752c1de5d05345b251ab077139421014daa635bc15a69bf5e717e30d8f870f3e6a96649bd97e837ca311555c1346015fd05e507fea0b3ce0bf8f0ef44d840b9aae3c4db4aa681979e18746adfd5c3c2e0cd917d99f75ffb9ea43770ab045861459d9107831c25abbf4219b106da412f2a64a735e0d3c854fddd4e85d80a1faa1d2796e41cc6d6c45d8e25b438259ba58a25e099596321e46870a4b5c16c822e02d269df7ae73ccb814c1File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -11,11 +11,11 @@ | ||||||||||||||||||||
| use Utopia\Database\Exception as DatabaseException; | ||||||||||||||||||||
| use Utopia\Database\Exception\Duplicate as DuplicateException; | ||||||||||||||||||||
| use Utopia\Database\Exception\NotFound as NotFoundException; | ||||||||||||||||||||
| use Utopia\Database\Exception\Order as OrderException; | ||||||||||||||||||||
| use Utopia\Database\Exception\Timeout as TimeoutException; | ||||||||||||||||||||
| use Utopia\Database\Exception\Truncate as TruncateException; | ||||||||||||||||||||
| use Utopia\Database\Helpers\ID; | ||||||||||||||||||||
| use Utopia\Database\Query; | ||||||||||||||||||||
| use Utopia\Database\QueryContext; | ||||||||||||||||||||
| use Utopia\Database\Validator\Authorization; | ||||||||||||||||||||
| class MariaDB extends SQL | ||||||||||||||||||||
| @@ -60,7 +60,7 @@ public function delete(string $name): bool | ||||||||||||||||||||
| $sql = "DROP DATABASE `{$name}`;"; | ||||||||||||||||||||
| $sql = $this->trigger(Database::EVENT_DATABASE_DELETE, $sql); | ||||||||||||||||||||
| var_dump($sql); | ||||||||||||||||||||
| return $this->getPDO() | ||||||||||||||||||||
| ->prepare($sql) | ||||||||||||||||||||
| ->execute(); | ||||||||||||||||||||
| @@ -1011,7 +1011,7 @@ public function createDocuments(string $collection, array $documents): array | ||||||||||||||||||||
| $columns = []; | ||||||||||||||||||||
| foreach ($attributeKeys as $key => $attribute) { | ||||||||||||||||||||
| $columns[$key] = "`{$this->filter($attribute)}`"; | ||||||||||||||||||||
| $columns[$key] = "{$this->quote($this->filter($attribute))}"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $columns = '(' . \implode(', ', $columns) . ')'; | ||||||||||||||||||||
| @@ -1673,121 +1673,144 @@ public function deleteDocument(string $collection, string $id): bool | ||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Find Documents | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * @param string $collection | ||||||||||||||||||||
| * @param QueryContext $context | ||||||||||||||||||||
| * @param array<Query> $queries | ||||||||||||||||||||
| * @param int|null $limit | ||||||||||||||||||||
| * @param int|null $offset | ||||||||||||||||||||
| * @param array<string> $orderAttributes | ||||||||||||||||||||
| * @param array<string> $orderTypes | ||||||||||||||||||||
| * @param array<string, mixed> $cursor | ||||||||||||||||||||
| * @param string $cursorDirection | ||||||||||||||||||||
| * @param string $forPermission | ||||||||||||||||||||
| * @param array<Query> $selects | ||||||||||||||||||||
| * @param array<Query> $filters | ||||||||||||||||||||
| * @param array<Query> $joins | ||||||||||||||||||||
| * @param array<Query> $orderQueries | ||||||||||||||||||||
| * @return array<Document> | ||||||||||||||||||||
| * @throws DatabaseException | ||||||||||||||||||||
| * @throws TimeoutException | ||||||||||||||||||||
| * @throws Exception | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array | ||||||||||||||||||||
| { | ||||||||||||||||||||
| $name = $this->filter($collection); | ||||||||||||||||||||
| public function find( | ||||||||||||||||||||
| QueryContext $context, | ||||||||||||||||||||
| array $queries = [], | ||||||||||||||||||||
| ?int $limit = 25, | ||||||||||||||||||||
| ?int $offset = null, | ||||||||||||||||||||
| array $cursor = [], | ||||||||||||||||||||
| string $cursorDirection = Database::CURSOR_AFTER, | ||||||||||||||||||||
| string $forPermission = Database::PERMISSION_READ, | ||||||||||||||||||||
| array $selects = [], | ||||||||||||||||||||
| array $filters = [], | ||||||||||||||||||||
| array $joins = [], | ||||||||||||||||||||
| array $orderQueries = [] | ||||||||||||||||||||
| ): array { | ||||||||||||||||||||
| unset($queries); // remove this since we pass explicit queries | ||||||||||||||||||||
| $alias = Query::DEFAULT_ALIAS; | ||||||||||||||||||||
| $binds = []; | ||||||||||||||||||||
| $name = $context->getCollections()[0]->getId(); | ||||||||||||||||||||
| $name = $this->filter($name); | ||||||||||||||||||||
| $roles = Authorization::getRoles(); | ||||||||||||||||||||
| $where = []; | ||||||||||||||||||||
| $orders = []; | ||||||||||||||||||||
| $alias = Query::DEFAULT_ALIAS; | ||||||||||||||||||||
| $binds = []; | ||||||||||||||||||||
| $queries = array_map(fn ($query) => clone $query, $queries); | ||||||||||||||||||||
| $filters = array_map(fn ($query) => clone $query, $filters); | ||||||||||||||||||||
| $hasIdAttribute = false; | ||||||||||||||||||||
| foreach ($orderAttributes as $i => $attribute) { | ||||||||||||||||||||
| $originalAttribute = $attribute; | ||||||||||||||||||||
| $cursorWhere = []; | ||||||||||||||||||||
| $attribute = $this->getInternalKeyForAttribute($attribute); | ||||||||||||||||||||
| foreach ($orderQueries as $i => $order) { | ||||||||||||||||||||
| $orderAlias = $order->getAlias(); | ||||||||||||||||||||
| $attribute = $order->getAttribute(); | ||||||||||||||||||||
| $originalAttribute = $attribute; | ||||||||||||||||||||
| $attribute = $this->getInternalKeyForAttribute($originalAttribute); | ||||||||||||||||||||
| $attribute = $this->filter($attribute); | ||||||||||||||||||||
| if (\in_array($attribute, ['_uid', '_id'])) { | ||||||||||||||||||||
| $hasIdAttribute = true; | ||||||||||||||||||||
| $direction = $order->getOrderDirection(); | ||||||||||||||||||||
| if ($cursorDirection === Database::CURSOR_BEFORE) { | ||||||||||||||||||||
| $direction = ($direction === Database::ORDER_ASC) ? Database::ORDER_DESC : Database::ORDER_ASC; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); | ||||||||||||||||||||
| $orders[] = "{$this->quote($attribute)} {$direction}"; | ||||||||||||||||||||
| // Get most dominant/first order attribute | ||||||||||||||||||||
| if ($i === 0 && !empty($cursor)) { | ||||||||||||||||||||
| $orderMethodInternalId = Query::TYPE_GREATER; // To preserve natural order | ||||||||||||||||||||
| $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; | ||||||||||||||||||||
| // Build pagination WHERE clause only if we have a cursor | ||||||||||||||||||||
| if (!empty($cursor)) { | ||||||||||||||||||||
| // Special case: No tie breaks. only 1 attribute and it's a unique primary key | ||||||||||||||||||||
| if (count($orderQueries) === 1 && $i === 0 && $originalAttribute === '$sequence') { | ||||||||||||||||||||
| $operator = ($direction === Database::ORDER_DESC) | ||||||||||||||||||||
| ? Query::TYPE_LESSER | ||||||||||||||||||||
| : Query::TYPE_GREATER; | ||||||||||||||||||||
| if ($cursorDirection === Database::CURSOR_BEFORE) { | ||||||||||||||||||||
| $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; | ||||||||||||||||||||
| $orderMethodInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; | ||||||||||||||||||||
| $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $bindName = ":cursor_pk"; | ||||||||||||||||||||
| $binds[$bindName] = $cursor[$originalAttribute]; | ||||||||||||||||||||
| if (\is_null($cursor[$originalAttribute] ?? null)) { | ||||||||||||||||||||
| throw new OrderException( | ||||||||||||||||||||
| message: "Order attribute '{$originalAttribute}' is empty", | ||||||||||||||||||||
| attribute: $originalAttribute | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| $cursorWhere[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; | ||||||||||||||||||||
| break; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $binds[':cursor'] = $cursor[$originalAttribute]; | ||||||||||||||||||||
| $where[] = "( | ||||||||||||||||||||
| {$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($orderMethod)} :cursor | ||||||||||||||||||||
| OR ( | ||||||||||||||||||||
| {$this->quote($alias)}.{$this->quote($attribute)} = :cursor | ||||||||||||||||||||
| AND | ||||||||||||||||||||
| {$this->quote($alias)}._id {$this->getSQLOperator($orderMethodInternalId)} {$cursor['$internalId']} | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| )"; | ||||||||||||||||||||
| } elseif ($cursorDirection === Database::CURSOR_BEFORE) { | ||||||||||||||||||||
| $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $conditions = []; | ||||||||||||||||||||
| $orders[] = "{$this->quote($attribute)} {$orderType}"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // Add equality conditions for previous attributes | ||||||||||||||||||||
| for ($j = 0; $j < $i; $j++) { | ||||||||||||||||||||
| $prevQuery = $orderQueries[$j]; | ||||||||||||||||||||
| $prevOriginal = $prevQuery->getAttribute(); | ||||||||||||||||||||
| $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal)); | ||||||||||||||||||||
| // Allow after pagination without any order | ||||||||||||||||||||
| if (empty($orderAttributes) && !empty($cursor)) { | ||||||||||||||||||||
| $orderType = $orderTypes[0] ?? Database::ORDER_ASC; | ||||||||||||||||||||
| $bindName = ":cursor_{$j}"; | ||||||||||||||||||||
| $binds[$bindName] = $cursor[$prevOriginal]; | ||||||||||||||||||||
| if ($cursorDirection === Database::CURSOR_AFTER) { | ||||||||||||||||||||
| $orderMethod = $orderType === Database::ORDER_DESC | ||||||||||||||||||||
| $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($prevAttr)} = {$bindName}"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // Add comparison for current attribute | ||||||||||||||||||||
| $operator = ($direction === Database::ORDER_DESC) | ||||||||||||||||||||
| ? Query::TYPE_LESSER | ||||||||||||||||||||
| : Query::TYPE_GREATER; | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| $orderMethod = $orderType === Database::ORDER_DESC | ||||||||||||||||||||
| ? Query::TYPE_GREATER | ||||||||||||||||||||
| : Query::TYPE_LESSER; | ||||||||||||||||||||
| $bindName = ":cursor_{$i}"; | ||||||||||||||||||||
| $binds[$bindName] = $cursor[$originalAttribute]; | ||||||||||||||||||||
| $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; | ||||||||||||||||||||
| $cursorWhere[] = '(' . implode(' AND ', $conditions) . ')'; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$internalId']})"; | ||||||||||||||||||||
| if (!empty($cursorWhere)) { | ||||||||||||||||||||
| $where[] = '(' . implode(' OR ', $cursorWhere) . ')'; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // Allow order type without any order attribute, fallback to the natural order (_id) | ||||||||||||||||||||
| if (!$hasIdAttribute) { | ||||||||||||||||||||
| if (empty($orderAttributes) && !empty($orderTypes)) { | ||||||||||||||||||||
| $order = $orderTypes[0] ?? Database::ORDER_ASC; | ||||||||||||||||||||
| if ($cursorDirection === Database::CURSOR_BEFORE) { | ||||||||||||||||||||
| $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $sqlJoin = ''; | ||||||||||||||||||||
| foreach ($joins as $join) { | ||||||||||||||||||||
| $permissions = ''; | ||||||||||||||||||||
| $collection = $join->getCollection(); | ||||||||||||||||||||
| $collection = $this->filter($collection); | ||||||||||||||||||||
| $orders[] = "{$this->quote($alias)}._id ".$this->filter($order); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| $orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' | ||||||||||||||||||||
| $skipAuth = $context->skipAuth($collection, $forPermission); | ||||||||||||||||||||
| if (! $skipAuth) { | ||||||||||||||||||||
| $permissions = 'AND '.$this->getSQLPermissionsCondition($collection, $roles, $join->getAlias(), $forPermission); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $sqlJoin .= "INNER JOIN {$this->getSQLTable($collection)} AS {$this->quote($join->getAlias())} | ||||||||||||||||||||
| ON {$this->getSQLConditions($join->getValues(), $binds)} | ||||||||||||||||||||
| {$permissions} | ||||||||||||||||||||
| {$this->getTenantQuery($collection, $join->getAlias())} | ||||||||||||||||||||
| "; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $conditions = $this->getSQLConditions($queries, $binds); | ||||||||||||||||||||
| $conditions = $this->getSQLConditions($filters, $binds); | ||||||||||||||||||||
| if (!empty($conditions)) { | ||||||||||||||||||||
| $where[] = $conditions; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (Authorization::$status) { | ||||||||||||||||||||
| $skipAuth = $context->skipAuth($name, $forPermission); | ||||||||||||||||||||
| if (! $skipAuth) { | ||||||||||||||||||||
| $where[] = $this->getSQLPermissionsCondition($name, $roles, $alias, $forPermission); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if ($this->sharedTables) { | ||||||||||||||||||||
| $binds[':_tenant'] = $this->tenant; | ||||||||||||||||||||
| $where[] = "{$this->getTenantQuery($collection, $alias, condition: '')}"; | ||||||||||||||||||||
| $where[] = "{$this->getTenantQuery($name, $alias, condition: '')}"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; | ||||||||||||||||||||
| @@ -1804,11 +1827,10 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, | ||||||||||||||||||||
| $sqlLimit .= ' OFFSET :offset'; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $selections = $this->getAttributeSelections($queries); | ||||||||||||||||||||
| $sql = " | ||||||||||||||||||||
| SELECT {$this->getAttributeProjection($selections, $alias)} | ||||||||||||||||||||
| SELECT {$this->getAttributeProjection($selects)} | ||||||||||||||||||||
| FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} | ||||||||||||||||||||
| {$sqlJoin} | ||||||||||||||||||||
| {$sqlWhere} | ||||||||||||||||||||
| {$sqlOrder} | ||||||||||||||||||||
| {$sqlLimit}; | ||||||||||||||||||||
| @@ -1823,14 +1845,16 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, | ||||||||||||||||||||
| $stmt->bindValue($key, $value, $this->getPDOType($value)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| echo $stmt->queryString; | ||||||||||||||||||||
| var_dump($binds); | ||||||||||||||||||||
| $stmt->execute(); | ||||||||||||||||||||
| $results = $stmt->fetchAll(); | ||||||||||||||||||||
| $stmt->closeCursor(); | ||||||||||||||||||||
Comment on lines
+1848
to
+1853
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Printing every generated SELECT and its bindings in - echo $stmt->queryString;- var_dump($binds);+ // Debug output removed – enable via logger if necessary📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||
| } catch (PDOException $e) { | ||||||||||||||||||||
| throw $this->processException($e); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| $results = $stmt->fetchAll(); | ||||||||||||||||||||
| $stmt->closeCursor(); | ||||||||||||||||||||
| foreach ($results as $index => $document) { | ||||||||||||||||||||
| if (\array_key_exists('_uid', $document)) { | ||||||||||||||||||||
| $results[$index]['$id'] = $document['_uid']; | ||||||||||||||||||||
| @@ -1956,6 +1980,7 @@ public function sum(string $collection, string $attribute, array $queries = [], | ||||||||||||||||||||
| $roles = Authorization::getRoles(); | ||||||||||||||||||||
| $where = []; | ||||||||||||||||||||
| $alias = Query::DEFAULT_ALIAS; | ||||||||||||||||||||
| $alias = Query::DEFAULT_ALIAS; | ||||||||||||||||||||
| $binds = []; | ||||||||||||||||||||
| $limit = ''; | ||||||||||||||||||||
| @@ -2023,11 +2048,14 @@ public function sum(string $collection, string $attribute, array $queries = [], | ||||||||||||||||||||
| protected function getSQLCondition(Query $query, array &$binds): string | ||||||||||||||||||||
| { | ||||||||||||||||||||
| $query->setAttribute($this->getInternalKeyForAttribute($query->getAttribute())); | ||||||||||||||||||||
| $query->setAttributeRight($this->getInternalKeyForAttribute($query->getAttributeRight())); | ||||||||||||||||||||
| $attribute = $query->getAttribute(); | ||||||||||||||||||||
| $attribute = $this->filter($attribute); | ||||||||||||||||||||
| $attribute = $this->quote($attribute); | ||||||||||||||||||||
| $alias = $this->quote(Query::DEFAULT_ALIAS); | ||||||||||||||||||||
| $alias = $query->getAlias(); | ||||||||||||||||||||
| $alias = $this->filter($alias); | ||||||||||||||||||||
| $alias = $this->quote($alias); | ||||||||||||||||||||
| $placeholder = ID::unique(); | ||||||||||||||||||||
| switch ($query->getMethod()) { | ||||||||||||||||||||
| @@ -2054,6 +2082,12 @@ protected function getSQLCondition(Query $query, array &$binds): string | ||||||||||||||||||||
| return "{$alias}.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1"; | ||||||||||||||||||||
| case Query::TYPE_RELATION_EQUAL: | ||||||||||||||||||||
| $attributeRight = $this->quote($this->filter($query->getAttributeRight())); | ||||||||||||||||||||
| $aliasRight = $this->quote($query->getRightAlias()); | ||||||||||||||||||||
| return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}"; | ||||||||||||||||||||
| case Query::TYPE_IS_NULL: | ||||||||||||||||||||
| case Query::TYPE_IS_NOT_NULL: | ||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug
var_dump($sql)left indelete()This will leak internal DDL statements and pollute stdout for every DB drop. Remove or guard behind a log-level flag.
- var_dump($sql);📝 Committable suggestion
🤖 Prompt for AI Agents