Skip to content
Draft
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
39 changes: 39 additions & 0 deletions src/Persistence/Sql/Optimizer/ParsedColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Persistence\Sql\Optimizer;

use Atk4\Data\Persistence\Sql\Expression;

class ParsedColumn
{
/** @var Expression|string */
public $expr;
/** @var string|null not-null iff expr is a string */
public $exprTableAlias;
/** @var string */
public $columnAlias;

public function __construct(Expression $expr, string $columnAlias)
{
$exprIdentifier = Util::tryParseIdentifier($expr);
if ($exprIdentifier !== false) {
$this->exprTableAlias = $exprIdentifier[0];
$this->expr = $exprIdentifier[1];
} else {
$this->expr = $expr;
}

$this->columnAlias = Util::parseSingleIdentifier($columnAlias);
}

public function getDsqlExpression(): Expression
{
if ($this->exprTableAlias !== null) {
return new Expression('{}.{} {}', [$this->expr, $this->exprTableAlias, $this->columnAlias]); // @phpstan-ignore-line @TODO not sure what to do here !!!
}

return new Expression('{} {}', [$this->expr, $this->columnAlias]); // @phpstan-ignore-line @TODO not sure what to do here !!!
}
}
38 changes: 38 additions & 0 deletions src/Persistence/Sql/Optimizer/ParsedSelect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Persistence\Sql\Optimizer;

use Atk4\Data\Persistence\Sql\Expression;
use Atk4\Data\Persistence\Sql\Expressionable;
use Atk4\Data\Persistence\Sql\Query;

class ParsedSelect implements Expressionable // remove Expressionable later
{
/** @var Query|string */
public $expr;
/** @var string|null */
public $tableAlias;

/**
* @param Query|string $expr
*/
public function __construct($expr, ?string $tableAlias)
{
$exprIdentifier = Util::tryParseIdentifier($expr);
if ($exprIdentifier !== false) {
$this->expr = Util::parseSingleIdentifier($expr);
} else {
$this->expr = $expr;
}

$this->tableAlias = $tableAlias !== null ? Util::parseSingleIdentifier($tableAlias) : null;
}

#[\Override]
public function getDsqlExpression(Expression $expression): Expression
{
return new Expression('{}', [$this->expr]); // @phpstan-ignore-line @TODO not sure what to do here !!!
}
}
172 changes: 172 additions & 0 deletions src/Persistence/Sql/Optimizer/Util.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Persistence\Sql\Optimizer;

use Atk4\Data\Persistence\Sql\Exception;
use Atk4\Data\Persistence\Sql\Expression;
use Atk4\Data\Persistence\Sql\Expressionable;
use Atk4\Data\Persistence\Sql\Query;

class Util
{
private function __construct() {}

/**
* @return string|false
*/
private static function tryUnquoteSingleIdentifier(string $str)
{
if (preg_match('~^[\w\x80-\xf7]+$~', $str)) { // unquoted identifier
return $str;
}

$openQuoteChar = substr($str, 0, 1);
$closeQuoteChar = $openQuoteChar;
if ($openQuoteChar === '[') { // for MSSQL
$strQuoteCharTrimmed = preg_replace('~^\[(.+)\]$~s', '$1', $str);
$closeQuoteChar = ']';
} else {
$strQuoteCharTrimmed = preg_replace('~^(["`])(.+)\1$~s', '$2', $str);
}

if ($str === $openQuoteChar . str_replace($openQuoteChar, $openQuoteChar . $openQuoteChar, $strQuoteCharTrimmed) . $closeQuoteChar) {
return $strQuoteCharTrimmed;
}

return false;
}

/**
* Parse a, a.b, a."b", "a"."b" etc.
*
* WARNING: it can return array if the input is part of another expression which uses it as a string value
*
* @param Expressionable|string $expr
*
* @return array{string|null, string}|false
*/
public static function tryParseIdentifier($expr)
{
if ($expr instanceof Query) {
return false;
} elseif ($expr instanceof Expression) {
$str = $expr->render()[0];
} elseif ($expr instanceof Expressionable) {
$str = (new Expression('[]', [$expr]))->render()[0]; // @phpstan-ignore-line @TODO not sure what to do here !!!
} else {
$str = $expr;
}

$parts = preg_split('~\.(?=["`[]|\w+$)~', $str, 2);
$parts[0] = self::tryUnquoteSingleIdentifier($parts[0]);
if ($parts[0] === false) {
return false;
}

if (isset($parts[1])) {
$parts[1] = self::tryUnquoteSingleIdentifier($parts[1]);
if ($parts[1] === false) {
return false;
}
} else {
array_unshift($parts, null);
}

return $parts;
}

/**
* Returns true only if the expression is a single identifier (a or "a", but not "a"."b" or *).
*
* WARNING: it can return true if the input is part of another expression which uses it as a string value
*
* @param Expressionable|string $expr
*/
public static function isSingleIdentifier($expr): bool
{
$v = static::tryParseIdentifier($expr);

return $v !== false && $v[0] === null;
}

/**
* @param Expressionable|string $expr
*/
public static function parseSingleIdentifier($expr): string
{
$v = static::tryParseIdentifier($expr);
if ($v === false) {
throw (new Exception('Invalid SQL identifier'))
->addMoreInfo('expr', $expr);
} elseif ($v[0] !== null) {
throw (new Exception('Single SQL identifier without table name required'))
->addMoreInfo('expr', $expr);
}

return $v[1];
}

/**
* @param string|null $alias
* @param mixed $v
*
* @return mixed
*/
public static function parseSelectQueryTraverseValue(Expression $exprFactory, string $argName, $alias, $v)
{
// expand all Expressionable objects to Expression
if ($v instanceof Expressionable && !$v instanceof Expression) {
$v = $v->getDsqlExpression($exprFactory);
}

if (is_array($v)) {
$res = [];
foreach ($v as $k => $v2) {
$res[$k] = static::parseSelectQueryTraverseValue($exprFactory, $argName, is_int($k) ? null : $k, $v2);
}

return $res;
} elseif ($v instanceof Query) {
return static::parseSelectQuery($v, $alias);
}

return $v;
}

public static function parseSelectQuery(Query $query, ?string $tableAlias): ParsedSelect
{
$query->args['is_select_parsed'] = [true];
$select = new ParsedSelect($query, $tableAlias);
if (is_string($select->expr)) {
return $select;
}

// traverse $query and parse everything into ParsedSelect/ParsedColumn
foreach ($query->args as $argName => $args) {
foreach ($args as $alias => $v) {
$query->args[$argName][$alias] = static::parseSelectQueryTraverseValue(
$query->expr(),
$argName,
is_int($alias) ? null : $alias,
$v
);
}
}

return $select;
}

public static function isSelectQueryParsed(Query $query): bool
{
return ($query->args['is_select_parsed'] ?? [])[0] ?? false;
}

public static function parseColumn(Expression $expr, string $columnAlias): ParsedColumn
{
$column = new ParsedColumn($expr, $columnAlias);

return $column;
}
}
10 changes: 5 additions & 5 deletions src/Persistence/Sql/Oracle/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public function render(): array
#[\Override]
protected function _subrenderCondition(array $row): string
{
if (count($row) === 2) {
[$field, $value] = $row;
$cond = '=';
} elseif (count($row) >= 3) {
if (count($row) === 3) {
[$field, $cond, $value] = $row;
if ($cond === null) {
$cond = '=';
}
}

if (count($row) >= 2 && $field instanceof Field
if (count($row) === 3 && $field instanceof Field
&& in_array($field->type, ['text', 'blob'], true)) {
if ($field->type === 'text') {
$field = $this->expr('LOWER([])', [$field]);
Expand Down
Loading