packages/ztd-query-sqlite/src/Sql/Returning/ReturningItemParser.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Sqlite\Sql\Returning;
6
7use ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile;
8use ZtdQuery\Sql\SqlToken;
9use ZtdQuery\Sql\SqlTokenKind;
10use ZtdQuery\Sql\SqlTokenStream;
11
12/**
13 * Parses RETURNING expressions and their output column names.
14 *
15 * @visibility ZtdQuery\Platform\Sqlite
16 */
17final class ReturningItemParser
18{
19 /**
20 * @return array{source: string|null, output: string|null}|null
21 */
22 public function parseItem(string $expression): ?array
23 {
24 $tokens = SqlTokenStream::tokenize($expression, SqliteLexerProfile::create())->significantTokens();
25 $alias = null;
26 $asIndex = $this->asIndex($tokens);
27 if ($asIndex !== null) {
28 if ($asIndex + 2 !== count($tokens)) {
29 return null;
30 }
31 $aliasToken = $tokens[$asIndex + 1] ?? null;
32 if ($aliasToken === null) {
33 return null;
34 }
35 $alias = $this->identifierName($aliasToken);
36 if ($alias === null) {
37 return null;
38 }
39 $tokens = array_slice($tokens, 0, $asIndex);
40 }
41
42 if (!$this->isIdentifierPath($tokens)) {
43 return null;
44 }
45 $last = $tokens[count($tokens) - 1];
46 if ($last->text === '*') {
47 return $alias === null ? ['source' => null, 'output' => null] : null;
48 }
49
50 $source = $this->identifierName($last);
51 if ($source === null) {
52 return null;
53 }
54
55 return ['source' => $source, 'output' => $alias];
56 }
57
58 /**
59 * @param list<SqlToken> $tokens
60 */
61 public function asIndex(array $tokens): ?int
62 {
63 foreach ($tokens as $index => $token) {
64 if ($token->isKeyword('AS')) {
65 return $index;
66 }
67 }
68
69 return null;
70 }
71
72 /**
73 * @param list<SqlToken> $tokens
74 */
75 public function isIdentifierPath(array $tokens): bool
76 {
77 $lastIndex = count($tokens) - 1;
78 foreach ($tokens as $index => $token) {
79 if ($index % 2 === 0) {
80 if ($this->identifierName($token) !== null) {
81 continue;
82 }
83
84 return $index === $lastIndex
85 && $token->kind === SqlTokenKind::Symbol
86 && $token->text === '*';
87 }
88 if ($token->kind !== SqlTokenKind::Symbol || $token->text !== '.') {
89 return false;
90 }
91 }
92
93 return true;
94 }
95
96 /**
97 * Parses RETURNING expressions and their output column names.
98 */
99 public function identifierName(SqlToken $token): ?string
100 {
101 if ($token->kind === SqlTokenKind::Word) {
102 return $token->text;
103 }
104 if ($token->kind !== SqlTokenKind::QuotedIdentifier || strlen($token->text) <= 2) {
105 return null;
106 }
107
108 $identifier = $token->text;
109 $first = $identifier[0];
110 if (!in_array($first, ['"', '`'], true) || $identifier[strlen($identifier) - 1] !== $first) {
111 return null;
112 }
113
114 return str_replace($first . $first, $first, substr($identifier, 1, -1));
115 }
116}
117