1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Persistence\Sql\Oracle;
6:
7: trait ExpressionTrait
8: {
9: /**
10: * Like mb_str_split() function, but split by length in bytes.
11: *
12: * @return array<string>
13: */
14: private function splitLongString(string $value, int $lengthBytes): array
15: {
16: $res = [];
17: $value = array_reverse(str_split($value, 2 * $lengthBytes));
18: $i = count($value) - 1;
19: $buffer = '';
20: while (true) {
21: if (strlen($buffer) <= $lengthBytes && $i >= 0) {
22: $buffer .= array_pop($value);
23: --$i;
24: }
25:
26: if (strlen($buffer) <= $lengthBytes) {
27: $res[] = $buffer;
28: $buffer = '';
29:
30: break;
31: }
32:
33: $l = $lengthBytes;
34: for ($j = 0; $j < 4; ++$j) {
35: $ordNextChar = ord(substr($buffer, $l - $j, 1));
36: if ($ordNextChar < 0x80 || $ordNextChar >= 0xC0) {
37: $l -= $j;
38:
39: break;
40: }
41: }
42: $res[] = substr($buffer, 0, $l);
43: $buffer = substr($buffer, $l);
44: }
45:
46: return $res;
47: }
48:
49: protected function convertLongStringToClobExpr(string $value): Expression
50: {
51: // Oracle (multibyte) string literal is limited to 1332 bytes
52: $parts = $this->splitLongString($value, 1000);
53:
54: $exprArgs = [];
55: $buildConcatExprFx = static function (array $parts) use (&$buildConcatExprFx, &$exprArgs): string {
56: if (count($parts) > 1) {
57: $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2));
58: $partsRight = array_slice($parts, count($partsLeft));
59:
60: return 'CONCAT(' . $buildConcatExprFx($partsLeft) . ', ' . $buildConcatExprFx($partsRight) . ')';
61: }
62:
63: $exprArgs[] = reset($parts);
64:
65: return 'TO_CLOB([])';
66: };
67:
68: $expr = $buildConcatExprFx($parts);
69:
70: // @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/9022
71: return $this->expr($expr, $exprArgs); // @phpstan-ignore-line
72: }
73:
74: #[\Override]
75: protected function updateRenderBeforeExecute(array $render): array
76: {
77: [$sql, $params] = parent::updateRenderBeforeExecute($render);
78:
79: $newParamBase = $this->paramBase;
80: $newParams = [];
81: $sql = preg_replace_callback(
82: '~(?!\')' . self::QUOTED_TOKEN_REGEX . '\K|' . self::QUOTED_TOKEN_REGEX . '|:\w+~',
83: function ($matches) use ($params, &$newParams, &$newParamBase) {
84: if ($matches[0] === '') {
85: return '';
86: }
87:
88: if (str_starts_with($matches[0], '\'')) {
89: $value = str_replace('\'\'', '\'', substr($matches[0], 1, -1));
90: if (strlen($value) <= 4000) {
91: return $matches[0];
92: }
93: } else {
94: $value = $params[$matches[0]];
95: }
96:
97: if (is_string($value) && strlen($value) > 4000) {
98: $expr = $this->convertLongStringToClobExpr($value);
99: unset($value);
100: [$exprSql, $exprParams] = $expr->render();
101: $sql = preg_replace_callback(
102: '~' . self::QUOTED_TOKEN_REGEX . '\K|:\w+~',
103: static function ($matches) use ($exprParams, &$newParams, &$newParamBase) {
104: if ($matches[0] === '') {
105: return '';
106: }
107:
108: $name = ':' . $newParamBase;
109: ++$newParamBase; // @phpstan-ignore-line
110: $newParams[$name] = $exprParams[$matches[0]];
111:
112: return $name;
113: },
114: $exprSql
115: );
116: } else {
117: $sql = ':' . $newParamBase;
118: ++$newParamBase; // @phpstan-ignore-line
119:
120: $newParams[$sql] = $value;
121:
122: // fix oci8 param type bind
123: // TODO create a DBAL PR - https://github.com/doctrine/dbal/blob/3.7.1/src/Driver/OCI8/Statement.php#L135
124: // fix pdo_oci param type bind
125: // https://github.com/php/php-src/issues/12578
126: if (is_bool($value) || is_int($value)) {
127: $sql = 'cast(' . $sql . ' as INTEGER)';
128: } elseif (is_float($value)) {
129: $sql = 'cast(' . $sql . ' as BINARY_DOUBLE)';
130: }
131: }
132:
133: return $sql;
134: },
135: $sql
136: );
137:
138: return [$sql, $newParams];
139: }
140: }
141: