1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Persistence\Sql\Mssql;
6:
7: use Atk4\Data\Persistence\Sql\ExecuteException;
8: use Doctrine\DBAL\Connection as DbalConnection;
9: use Doctrine\DBAL\Driver\PDO\Exception as DbalDriverPdoException;
10: use Doctrine\DBAL\Driver\PDO\Result as DbalDriverPdoResult;
11: use Doctrine\DBAL\Result as DbalResult;
12:
13: trait ExpressionTrait
14: {
15: #[\Override]
16: public function render(): array
17: {
18: [$sql, $params] = parent::render();
19:
20: // convert all string literals to NVARCHAR, eg. 'text' to N'text'
21: $sql = preg_replace_callback(
22: '~(?!\')' . self::QUOTED_TOKEN_REGEX . '\K|N?' . self::QUOTED_TOKEN_REGEX . '~',
23: static function ($matches) {
24: if ($matches[0] === '') {
25: return '';
26: }
27:
28: return (substr($matches[0], 0, 1) === 'N' ? '' : 'N') . $matches[0];
29: },
30: $sql
31: );
32:
33: return [$sql, $params];
34: }
35:
36: #[\Override]
37: protected function hasNativeNamedParamSupport(): bool
38: {
39: return false;
40: }
41:
42: #[\Override]
43: protected function updateRenderBeforeExecute(array $render): array
44: {
45: [$sql, $params] = $render;
46:
47: $sql = preg_replace_callback(
48: '~' . self::QUOTED_TOKEN_REGEX . '\K|:\w+~',
49: static function ($matches) use ($params) {
50: if ($matches[0] === '') {
51: return '';
52: }
53:
54: $sql = $matches[0];
55: $value = $params[$sql];
56:
57: // emulate bind param support for float type
58: // TODO open php-src feature request
59: if (is_float($value)) {
60: $sql = 'cast(' . $sql . ' as DOUBLE PRECISION)';
61: }
62:
63: return $sql;
64: },
65: $sql
66: );
67:
68: return parent::updateRenderBeforeExecute([$sql, $params]);
69: }
70:
71: #[\Override]
72: protected function _execute(?object $connection, bool $fromExecuteStatement)
73: {
74: // fix exception throwing for MSSQL TRY/CATCH SQL (for Query::$templateInsert)
75: // https://github.com/microsoft/msphpsql/issues/1387
76: if ($fromExecuteStatement && $connection instanceof DbalConnection) {
77: // mimic https://github.com/doctrine/dbal/blob/3.7.1/src/Statement.php#L249
78: $result = $this->_execute($connection, false);
79:
80: $driverResult = \Closure::bind(static fn (): DbalDriverPdoResult => $result->result, null, DbalResult::class)(); // @phpstan-ignore-line
81: $driverPdoResult = \Closure::bind(static fn () => $driverResult->statement, null, DbalDriverPdoResult::class)();
82: try {
83: while ($driverPdoResult->nextRowset());
84: } catch (\PDOException $e) {
85: $e = $connection->convertException(DbalDriverPdoException::new($e));
86:
87: $firstException = $e;
88: while ($firstException->getPrevious() !== null) {
89: $firstException = $firstException->getPrevious();
90: }
91: $errorInfo = $firstException instanceof \PDOException ? $firstException->errorInfo : null;
92:
93: $eNew = (new ExecuteException('Dsql execute error', $errorInfo[1] ?? $e->getCode(), $e));
94: if ($errorInfo !== null && $errorInfo !== []) {
95: $eNew->addMoreInfo('error', $errorInfo[2] ?? 'n/a (' . $errorInfo[0] . ')');
96: }
97: $eNew->addMoreInfo('query', $this->getDebugQuery());
98:
99: throw $eNew;
100: }
101:
102: return $result->rowCount();
103: }
104:
105: return parent::_execute($connection, $fromExecuteStatement);
106: }
107: }
108: