• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 28352816058

29 Jun 2026 06:22AM UTC coverage: 89.118% (+0.02%) from 89.101%
28352816058

push

github

web-flow
feat: add Query Builder `sharedLock()` (#10315)

* feat(database): add shared select locks

- Add Query Builder sharedLock() for pessimistic read locks.
- Compile driver-specific shared-lock SQL for supported drivers.
- Preserve SELECT lock state around exists() and countAllResults().
- Document driver support and limitations.
- Cover generated SQL, reset behavior, unsupported drivers, and edge cases.

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>

* retrigger CI

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>

---------

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>

78 of 79 new or added lines in 6 files covered. (98.73%)

25011 of 28065 relevant lines covered (89.12%)

224.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.22
/system/Database/MySQLi/Builder.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Database\MySQLi;
15

16
use CodeIgniter\Database\BaseBuilder;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\RawSql;
19

20
/**
21
 * Builder for MySQLi
22
 */
23
class Builder extends BaseBuilder
24
{
25
    /**
26
     * Identifier escape character
27
     *
28
     * @var string
29
     */
30
    protected $escapeChar = '`';
31

32
    /**
33
     * Specifies which sql statements
34
     * support the ignore option.
35
     *
36
     * @var array<string, string>
37
     */
38
    protected $supportedIgnoreStatements = [
39
        'update' => 'IGNORE',
40
        'insert' => 'IGNORE',
41
        'delete' => 'IGNORE',
42
    ];
43

44
    /**
45
     * FROM tables
46
     *
47
     * Groups tables in FROM clauses if needed, so there is no confusion
48
     * about operator precedence.
49
     *
50
     * Note: This is only used (and overridden) by MySQL.
51
     */
52
    protected function _fromTables(): string
53
    {
54
        if ($this->QBJoin !== [] && count($this->QBFrom) > 1) {
906✔
55
            return '(' . implode(', ', $this->QBFrom) . ')';
×
56
        }
57

58
        return implode(', ', $this->QBFrom);
906✔
59
    }
60

61
    /**
62
     * Compile the SELECT lock clause.
63
     */
64
    protected function compileSelectLock(): string
65
    {
66
        if ($this->QBSelectLock === null) {
906✔
67
            return '';
905✔
68
        }
69

70
        foreach ($this->QBFrom as $value) {
3✔
71
            if (str_starts_with($value, '(SELECT')) {
3✔
72
                throw new DatabaseException(sprintf(
2✔
73
                    'MySQLi does not support %s() with fromSubquery().',
2✔
74
                    $this->selectLockMethod(),
2✔
75
                ));
2✔
76
            }
77
        }
78

79
        if ($this->QBSelectLock === self::SELECT_LOCK_SHARED) {
1✔
80
            return "\nLOCK IN SHARE MODE";
1✔
81
        }
82

NEW
83
        return parent::compileSelectLock();
×
84
    }
85

86
    /**
87
     * Generates a platform-specific batch update string from the supplied data
88
     */
89
    protected function _updateBatch(string $table, array $keys, array $values): string
90
    {
91
        $sql = $this->QBOptions['sql'] ?? '';
18✔
92

93
        // if this is the first iteration of batch then we need to build skeleton sql
94
        if ($sql === '') {
18✔
95
            $constraints = $this->QBOptions['constraints'] ?? [];
18✔
96

97
            if ($constraints === []) {
18✔
98
                if ($this->db->DBDebug) {
1✔
99
                    throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
100
                }
101

102
                return ''; // @codeCoverageIgnore
103
            }
104

105
            $updateFields = $this->QBOptions['updateFields'] ??
17✔
106
                $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
17✔
107
                [];
14✔
108

109
            $alias = $this->QBOptions['alias'] ?? '`_u`';
17✔
110

111
            $sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
17✔
112

113
            $sql .= "INNER JOIN (\n{:_table_:}";
17✔
114

115
            $sql .= ') ' . $alias . "\n";
17✔
116

117
            $sql .= 'ON ' . implode(
17✔
118
                ' AND ',
17✔
119
                array_map(
17✔
120
                    static fn ($key, $value) => (
17✔
121
                        ($value instanceof RawSql && is_string($key))
17✔
122
                        ?
17✔
123
                        $table . '.' . $key . ' = ' . $value
1✔
124
                        :
17✔
125
                        (
17✔
126
                            $value instanceof RawSql
16✔
127
                            ?
16✔
128
                            $value
3✔
129
                            :
16✔
130
                            $table . '.' . $value . ' = ' . $alias . '.' . $value
17✔
131
                        )
17✔
132
                    ),
17✔
133
                    array_keys($constraints),
17✔
134
                    $constraints,
17✔
135
                ),
17✔
136
            ) . "\n";
17✔
137

138
            $sql .= "SET\n";
17✔
139

140
            $sql .= implode(
17✔
141
                ",\n",
17✔
142
                array_map(
17✔
143
                    static fn ($key, $value): string => $table . '.' . $key . ($value instanceof RawSql ?
17✔
144
                        ' = ' . $value :
2✔
145
                        ' = ' . $alias . '.' . $value),
17✔
146
                    array_keys($updateFields),
17✔
147
                    $updateFields,
17✔
148
                ),
17✔
149
            );
17✔
150

151
            $this->QBOptions['sql'] = $sql;
17✔
152
        }
153

154
        if (isset($this->QBOptions['setQueryAsData'])) {
17✔
155
            $data = $this->QBOptions['setQueryAsData'];
1✔
156
        } else {
157
            $data = implode(
16✔
158
                " UNION ALL\n",
16✔
159
                array_map(
16✔
160
                    static fn ($value): string => 'SELECT ' . implode(', ', array_map(
16✔
161
                        static fn ($key, $index): string => $index . ' ' . $key,
16✔
162
                        $keys,
16✔
163
                        $value,
16✔
164
                    )),
16✔
165
                    $values,
16✔
166
                ),
16✔
167
            ) . "\n";
16✔
168
        }
169

170
        return str_replace('{:_table_:}', $data, $sql);
17✔
171
    }
172
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc