Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
- Added rate limit response headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After`)
- Added route-level metadata support on `Route` and `RouteBuilder` for rate limiting
- Added unit test coverage for router integration, middleware flow, adapters, factory resolution, and exception behavior
- SleekDB adapter now supports related-model criteria path filtering across join depths while preserving DBAL public API (#508)

### Removed
- Support for PHP 7.3 and earlier versions
Expand Down
209 changes: 158 additions & 51 deletions src/Database/Adapters/Sleekdb/SleekDbal.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

namespace Quantum\Database\Adapters\Sleekdb;

use Quantum\Database\Adapters\Sleekdb\Statements\RelatedCriteria;
use Quantum\Database\Adapters\Sleekdb\Statements\Criteria;
use Quantum\Database\Adapters\Sleekdb\Statements\Reducer;
use Quantum\Database\Adapters\Sleekdb\Statements\Result;
Expand All @@ -40,6 +41,7 @@ class SleekDbal implements DbalInterface
use Model;
use Result;
use Criteria;
use RelatedCriteria;
use Reducer;
use Join;

Expand Down Expand Up @@ -95,6 +97,28 @@ class SleekDbal implements DbalInterface
*/
protected $joins = [];

/**
* @var array<int, array<int|string, mixed>|string>
*/
protected array $rootCriterias = [];

/**
* @var array<string, array<int, array{0:string,1:string,2:mixed}>>
*/
protected array $relatedCriteriasByPath = [];

/**
* @var array<int, string>
*/
protected array $requiredRelatedPaths = [];

protected bool $criteriaPrepared = false;

/**
* @var array<int, string>
*/
protected array $autoSelectedRelatedRoots = [];

/**
* Associated model name
*/
Expand Down Expand Up @@ -122,13 +146,12 @@ class SleekDbal implements DbalInterface
*/
public array $hidden = [];

/**
* ORM Model
*/
private ?Store $ormModel = null;

private ?QueryBuilder $queryBuilder = null;

private bool $builderPrepared = false;

/**
* Active connection
* @var array<string, mixed>|null
Expand Down Expand Up @@ -231,11 +254,7 @@ public function getTable(): string

/**
* Gets the ORM model
* @throws DatabaseException
* @throws IOException
* @throws InvalidArgumentException
* @throws InvalidConfigurationException
* @throws BaseException
* @throws DatabaseException|BaseException|IOException|InvalidArgumentException|InvalidConfigurationException
*/
public function getOrmModel(): Store
{
Expand Down Expand Up @@ -283,87 +302,175 @@ public function truncate(): bool

/**
* Gets the query builder object
* @throws ModelException|DatabaseException|BaseException|IOException|InvalidArgumentException|InvalidConfigurationException
*/
public function getBuilder(): QueryBuilder
{
$builder = $this->getQueryBuilder();
if (!$this->builderPrepared) {
$this->prepareCriteriaScopesIfNeeded();
$this->applyBuilderModifiers($builder);
$this->builderPrepared = true;
}
return $builder;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Gets foreign keys
* @return array<string, array<string, mixed>>
*/
public function getForeignKeys(): array
{
return $this->foreignKeys;
}

/**
* Gets the associated model name
*/
public function getModelName(): string
{
return $this->modelName ?? '';
}

/**
* Resets the builder state
*/
protected function resetBuilderState(): void
{
$this->criterias = [];
$this->rootCriterias = [];
$this->relatedCriteriasByPath = [];
$this->requiredRelatedPaths = [];
$this->criteriaPrepared = false;
$this->autoSelectedRelatedRoots = [];
$this->havings = [];
$this->selected = [];
$this->grouped = [];
$this->ordered = [];
$this->offset = null;
$this->limit = null;
$this->joins = [];
$this->queryBuilder = null;
$this->builderPrepared = false;
}

/**
* Builds a dot-notated join path from current path and next relation table.
*/
protected function buildJoinPath(string $currentPath, string $table): string
{
return $currentPath !== '' ? $currentPath . '.' . $table : $table;
}

/**
* Ensures a reusable query builder instance exists for current adapter state.
* @return QueryBuilder
* @throws BaseException
* @throws DatabaseException
* @throws IOException
* @throws InvalidArgumentException
* @throws InvalidConfigurationException
* @throws ModelException
*/
public function getBuilder(): QueryBuilder
protected function getQueryBuilder(): QueryBuilder
{
$builder = $this->queryBuilder;
if ($this->queryBuilder === null) {
$this->queryBuilder = $this->getOrmModel()->createQueryBuilder();
}

return $this->queryBuilder;
}

if (!$builder) {
$builder = $this->getOrmModel()->createQueryBuilder();
$this->queryBuilder = $builder;
/**
* Prepares root/related criteria scopes once per builder lifecycle.
*/
protected function prepareCriteriaScopesIfNeeded(): void
{
if (!$this->criteriaPrepared) {
$this->prepareCriteriaScopes();
}
}

/**
* Applies all collected query modifiers on the given builder.
* @throws ModelException|InvalidArgumentException
*/
protected function applyBuilderModifiers(QueryBuilder $builder): void
{
$this->applySelectModifier($builder);
$this->applyJoinModifier();
$this->applyWhereModifier($builder);
$this->applyHavingModifier($builder);
$this->applyGroupModifier($builder);
$this->applyOrderModifier($builder);
$this->applyPaginationModifier($builder);
}

protected function applySelectModifier(QueryBuilder $builder): void
{
if ($this->selected !== []) {
$builder->select($this->selected);
$builder->select($this->buildSelectForQuery());
}
}

/**
* @throws ModelException
*/
protected function applyJoinModifier(): void
{
if ($this->joins !== []) {
$this->applyJoins();
}
}

if ($this->criterias !== []) {
$builder->where($this->criterias);
/**
* @throws InvalidArgumentException
*/
protected function applyWhereModifier(QueryBuilder $builder): void
{
if ($this->rootCriterias !== []) {
$builder->where($this->rootCriterias);
}
}

/**
* @throws InvalidArgumentException
*/
protected function applyHavingModifier(QueryBuilder $builder): void
{
if ($this->havings !== []) {
$builder->having($this->havings);
}
}

protected function applyGroupModifier(QueryBuilder $builder): void
{
if ($this->grouped !== []) {
$builder->groupBy($this->grouped);
}
}

/**
* @throws InvalidArgumentException
*/
protected function applyOrderModifier(QueryBuilder $builder): void
{
if ($this->ordered !== []) {
$builder->orderBy($this->ordered);
}
}

/**
* @throws InvalidArgumentException
*/
protected function applyPaginationModifier(QueryBuilder $builder): void
{
if ($this->offset) {
$builder->skip($this->offset);
}

if ($this->limit) {
$builder->limit($this->limit);
}

return $builder;
}

/**
* Gets foreign keys
* @return array<string, array<string, mixed>>
*/
public function getForeignKeys(): array
{
return $this->foreignKeys;
}

/**
* Gets the associated model name
*/
public function getModelName(): string
{
return $this->modelName ?? '';
}

/**
* Resets the builder state
*/
protected function resetBuilderState(): void
{
$this->criterias = [];
$this->havings = [];
$this->selected = [];
$this->grouped = [];
$this->ordered = [];
$this->offset = null;
$this->limit = null;
$this->joins = [];
$this->queryBuilder = null;
}
}
36 changes: 30 additions & 6 deletions src/Database/Adapters/Sleekdb/Statements/Join.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
*/
trait Join
{
abstract protected function buildJoinPath(string $currentPath, string $table): string;

/**
* @inheritDoc
*/
Expand All @@ -55,7 +57,7 @@ private function applyJoins(): void
throw new RuntimeException('Cannot apply joins without an initialized query builder.');
}

$this->applyJoin($this->queryBuilder, $this, $this->joins[0]);
$this->applyJoin($this->queryBuilder, $this, $this->joins[0], 1, '');
}
}

Expand All @@ -64,16 +66,23 @@ private function applyJoins(): void
* @param array<string, mixed> $nextItem
* @throws ModelException
*/
private function applyJoin(QueryBuilder $queryBuilder, SleekDbal $currentItem, array $nextItem, int $level = 1): QueryBuilder
{
private function applyJoin(
QueryBuilder $queryBuilder,
SleekDbal $currentItem,
array $nextItem,
int $level = 1,
string $currentPath = ''
): QueryBuilder {
$modelToJoin = unserialize($nextItem['model']);
$switch = $nextItem['switch'];

if (!$modelToJoin instanceof DbModel) {
throw new RuntimeException('Failed to unserialize join model.');
}

$queryBuilder->join(function ($item) use ($currentItem, $modelToJoin, $switch, $level) {
$joinPath = $this->buildJoinPath($currentPath, $modelToJoin->table);

$queryBuilder->join(function ($item) use ($currentItem, $modelToJoin, $switch, $level, $joinPath) {

$sleekModel = new self(
$modelToJoin->table,
Expand All @@ -85,22 +94,37 @@ private function applyJoin(QueryBuilder $queryBuilder, SleekDbal $currentItem, a
$newQueryBuilder = $sleekModel->getOrmModel()->createQueryBuilder();

$this->applyJoinTo($newQueryBuilder, $modelToJoin, $currentItem, $item);
$this->applyRelatedCriteriaToJoin($newQueryBuilder, $joinPath);

if ($switch && isset($this->joins[$level])) {
$this->applyJoin($newQueryBuilder, $sleekModel, $this->joins[$level], $level + 1);
$this->applyJoin($newQueryBuilder, $sleekModel, $this->joins[$level], $level + 1, $joinPath);
}

return $newQueryBuilder;

}, $modelToJoin->table);

if (!$switch && isset($this->joins[$level])) {
$this->applyJoin($queryBuilder, $currentItem, $this->joins[$level], $level + 1);
$this->applyJoin($queryBuilder, $currentItem, $this->joins[$level], $level + 1, $currentPath);
}

return $queryBuilder;
}

/**
* Applies criteria assigned to a specific join path on that join subquery.
*/
private function applyRelatedCriteriaToJoin(QueryBuilder $queryBuilder, string $joinPath): void
{
if (!isset($this->relatedCriteriasByPath[$joinPath])) {
return;
}

foreach ($this->relatedCriteriasByPath[$joinPath] as $criteria) {
$queryBuilder->where($criteria);
}
}

/**
* Apply join condition for JOINTO type
* @param array<string, mixed> $currentItem
Expand Down
Loading
Loading