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

codeigniter4 / CodeIgniter4 / 25902734269

15 May 2026 05:51AM UTC coverage: 88.459% (+0.2%) from 88.299%
25902734269

Pull #10159

github

web-flow
Merge f0573f3e0 into 170b89a6e
Pull Request #10159: feat: Add support for callable TTLs in cache handlers

6 of 10 new or added lines in 3 files covered. (60.0%)

446 existing lines in 24 files now uncovered.

24114 of 27260 relevant lines covered (88.46%)

219.07 hits per line

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

73.93
/system/Database/Postgre/Connection.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\Postgre;
15

16
use CodeIgniter\Database\BaseConnection;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\RawSql;
19
use CodeIgniter\Database\TableName;
20
use ErrorException;
21
use PgSql\Connection as PgSqlConnection;
22
use PgSql\Result as PgSqlResult;
23
use stdClass;
24
use Stringable;
25

26
/**
27
 * Connection for Postgre
28
 *
29
 * @extends BaseConnection<PgSqlConnection, PgSqlResult>
30
 */
31
class Connection extends BaseConnection
32
{
33
    /**
34
     * Database driver
35
     *
36
     * @var string
37
     */
38
    public $DBDriver = 'Postgre';
39

40
    /**
41
     * Database schema
42
     *
43
     * @var string
44
     */
45
    public $schema = 'public';
46

47
    /**
48
     * Identifier escape character
49
     *
50
     * @var string
51
     */
52
    public $escapeChar = '"';
53

54
    protected $connect_timeout;
55
    protected $options;
56
    protected $sslmode;
57
    protected $service;
58

59
    /**
60
     * Last failed query result, used by error() to extract the SQLSTATE
61
     * via pg_result_error_field() without string parsing.
62
     */
63
    private ?PgSqlResult $lastFailedResult = null;
64

65
    /**
66
     * Checks whether the native database error represents a unique constraint violation.
67
     */
68
    protected function isUniqueConstraintViolation(int|string $code, string $message): bool
69
    {
70
        return $code === '23505';
49✔
71
    }
72

73
    /**
74
     * Checks whether the native database code represents a retryable transaction failure.
75
     */
76
    protected function isRetryableTransactionErrorCode(int|string $code): bool
77
    {
78
        return in_array($code, ['40001', '40P01'], true);
20✔
79
    }
80

81
    /**
82
     * Connect to the database.
83
     *
84
     * @return false|PgSqlConnection
85
     */
86
    public function connect(bool $persistent = false)
87
    {
88
        if (empty($this->DSN)) {
59✔
89
            $this->buildDSN();
57✔
90
        }
91

92
        // Convert DSN string
93
        // @TODO This format is for PDO_PGSQL.
94
        //      https://www.php.net/manual/en/ref.pdo-pgsql.connection.php
95
        //      Should deprecate?
96
        if (mb_strpos($this->DSN, 'pgsql:') === 0) {
59✔
UNCOV
97
            $this->convertDSN();
×
98
        }
99

100
        $this->connID = $persistent ? pg_pconnect($this->DSN) : pg_connect($this->DSN);
59✔
101

102
        if ($this->connID !== false) {
59✔
103
            if (
104
                $persistent
59✔
105
                && pg_connection_status($this->connID) === PGSQL_CONNECTION_BAD
59✔
106
                && pg_ping($this->connID) === false
59✔
107
            ) {
UNCOV
108
                $error = pg_last_error($this->connID);
×
109

UNCOV
110
                throw new DatabaseException($error);
×
111
            }
112

113
            if (! empty($this->schema)) {
59✔
114
                $this->simpleQuery("SET search_path TO {$this->schema},public");
59✔
115
            }
116

117
            if ($this->setClientEncoding($this->charset) === false) {
59✔
118
                $error = pg_last_error($this->connID);
1✔
119

120
                throw new DatabaseException($error);
1✔
121
            }
122

123
            // Set session timezone if configured
124
            $timezoneOffset = $this->getSessionTimezone();
58✔
125
            if ($timezoneOffset !== null) {
58✔
126
                $this->simpleQuery("SET TIME ZONE '{$timezoneOffset}'");
3✔
127
            }
128
        }
129

130
        return $this->connID;
58✔
131
    }
132

133
    /**
134
     * Converts the DSN with semicolon syntax.
135
     *
136
     * @return void
137
     */
138
    private function convertDSN()
139
    {
140
        // Strip pgsql
141
        $this->DSN = mb_substr($this->DSN, 6);
5✔
142

143
        // Convert semicolons to spaces in DSN format like:
144
        // pgsql:host=localhost;port=5432;dbname=database_name
145
        // https://www.php.net/manual/en/function.pg-connect.php
146
        $allowedParams = ['host', 'port', 'dbname', 'user', 'password', 'connect_timeout', 'options', 'sslmode', 'service'];
5✔
147

148
        $parameters = explode(';', $this->DSN);
5✔
149

150
        $output            = '';
5✔
151
        $previousParameter = '';
5✔
152

153
        foreach ($parameters as $parameter) {
5✔
154
            [$key, $value] = explode('=', $parameter, 2);
5✔
155
            if (in_array($key, $allowedParams, true)) {
5✔
156
                if ($previousParameter !== '') {
5✔
157
                    if (array_search($key, $allowedParams, true) < array_search($previousParameter, $allowedParams, true)) {
5✔
158
                        $output .= ';';
1✔
159
                    } else {
160
                        $output .= ' ';
5✔
161
                    }
162
                }
163
                $output .= $parameter;
5✔
164
                $previousParameter = $key;
5✔
165
            } else {
166
                $output .= ';' . $parameter;
1✔
167
            }
168
        }
169

170
        $this->DSN = $output;
5✔
171
    }
172

173
    /**
174
     * Close the database connection.
175
     *
176
     * @return void
177
     */
178
    protected function _close()
179
    {
180
        pg_close($this->connID);
3✔
181
    }
182

183
    /**
184
     * Ping the database connection.
185
     */
186
    protected function _ping(): bool
187
    {
188
        return pg_ping($this->connID);
4✔
189
    }
190

191
    /**
192
     * Select a specific database table to use.
193
     */
194
    public function setDatabase(string $databaseName): bool
195
    {
UNCOV
196
        return false;
×
197
    }
198

199
    /**
200
     * Returns a string containing the version of the database being used.
201
     */
202
    public function getVersion(): string
203
    {
204
        if (isset($this->dataCache['version'])) {
7✔
205
            return $this->dataCache['version'];
6✔
206
        }
207

208
        if (! $this->connID) {
4✔
UNCOV
209
            $this->initialize();
×
210
        }
211

212
        $pgVersion                  = pg_version($this->connID);
4✔
213
        $this->dataCache['version'] = isset($pgVersion['server']) ?
4✔
214
            (preg_match('/^(\d+\.\d+)/', $pgVersion['server'], $matches) ? $matches[1] : '') :
4✔
UNCOV
215
            '';
×
216

217
        return $this->dataCache['version'];
4✔
218
    }
219

220
    /**
221
     * Executes the query against the database.
222
     *
223
     * @return false|PgSqlResult
224
     */
225
    protected function execute(string $sql)
226
    {
227
        $this->lastFailedResult = null;
844✔
228

229
        try {
230
            $sent = pg_send_query($this->connID, $sql);
844✔
UNCOV
231
        } catch (ErrorException $e) {
×
232
            $trace = array_slice($e->getTrace(), 2); // remove the call to error handler
×
233

234
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
×
UNCOV
235
                'message' => $e->getMessage(),
×
UNCOV
236
                'exFile'  => clean_path($e->getFile()),
×
UNCOV
237
                'exLine'  => $e->getLine(),
×
238
                'trace'   => render_backtrace($trace),
×
UNCOV
239
            ]);
×
240

UNCOV
241
            $exception = new DatabaseException($e->getMessage(), 0, $e);
×
242

UNCOV
243
            if ($this->DBDebug) {
×
244
                throw $exception;
×
245
            }
246

UNCOV
247
            $this->lastException = $exception;
×
248

UNCOV
249
            return false;
×
250
        }
251

252
        if ($sent === false) {
844✔
UNCOV
253
            return $this->handleConnectionError();
×
254
        }
255

256
        $result = pg_get_result($this->connID);
844✔
257

258
        if ($result === false) {
844✔
UNCOV
259
            return $this->handleConnectionError();
×
260
        }
261

262
        // Drain all results; return the last one (pg_query() semantics) or the first error.
263
        $lastResult   = $result;
844✔
264
        $failedResult = pg_result_status($result) === PGSQL_FATAL_ERROR ? $result : null;
844✔
265

266
        while (($next = pg_get_result($this->connID)) !== false) {
844✔
UNCOV
267
            $lastResult = $next;
×
268

UNCOV
269
            if (! $failedResult instanceof PgSqlResult && pg_result_status($next) === PGSQL_FATAL_ERROR) {
×
UNCOV
270
                $failedResult = $next;
×
271
            }
272
        }
273

274
        if ($failedResult instanceof PgSqlResult) {
844✔
275
            $sqlstate = (string) pg_result_error_field($failedResult, PGSQL_DIAG_SQLSTATE);
39✔
276
            $message  = (string) pg_result_error($failedResult);
39✔
277
            $trace    = debug_backtrace();
39✔
278
            $first    = array_shift($trace);
39✔
279

280
            $this->lastFailedResult = $failedResult;
39✔
281

282
            // Log only the first line; pg_result_error() appends "LINE N: ..." context.
283
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
39✔
284
                'message' => explode("\n", $message)[0],
39✔
285
                'exFile'  => clean_path($first['file']),
39✔
286
                'exLine'  => $first['line'],
39✔
287
                'trace'   => render_backtrace($trace),
39✔
288
            ]);
39✔
289

290
            $exception = $this->createDatabaseException($message, $sqlstate);
39✔
291

292
            if ($this->DBDebug) {
39✔
293
                throw $exception;
17✔
294
            }
295

296
            $this->lastException = $exception;
22✔
297

298
            return false;
22✔
299
        }
300

301
        return $lastResult;
844✔
302
    }
303

304
    /**
305
     * Logs a connection-level error from pg_last_error(), stores or throws a
306
     * DatabaseException, and returns false.
307
     */
308
    private function handleConnectionError(): false
309
    {
310
        $message = pg_last_error($this->connID);
×
311
        $trace   = debug_backtrace();
×
UNCOV
312
        $first   = array_shift($trace);
×
313

314
        log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
×
UNCOV
315
            'message' => $message,
×
316
            'exFile'  => clean_path($first['file']),
×
UNCOV
317
            'exLine'  => $first['line'],
×
UNCOV
318
            'trace'   => render_backtrace($trace),
×
UNCOV
319
        ]);
×
320

UNCOV
321
        $exception = new DatabaseException($message);
×
322

UNCOV
323
        if ($this->DBDebug) {
×
324
            throw $exception;
×
325
        }
326

UNCOV
327
        $this->lastException = $exception;
×
328

UNCOV
329
        return false;
×
330
    }
331

332
    /**
333
     * Get the prefix of the function to access the DB.
334
     */
335
    protected function getDriverFunctionPrefix(): string
336
    {
UNCOV
337
        return 'pg_';
×
338
    }
339

340
    /**
341
     * Returns the total number of rows affected by this query.
342
     */
343
    public function affectedRows(): int
344
    {
345
        if ($this->resultID === false) {
51✔
346
            return 0;
1✔
347
        }
348

349
        return pg_affected_rows($this->resultID);
50✔
350
    }
351

352
    /**
353
     * "Smart" Escape String
354
     *
355
     * Escapes data based on type
356
     *
357
     * @param array|bool|float|int|object|string|null $str
358
     *
359
     * @return ($str is array ? array : float|int|string)
360
     */
361
    public function escape($str)
362
    {
363
        if (! $this->connID) {
816✔
UNCOV
364
            $this->initialize();
×
365
        }
366

367
        if ($str instanceof Stringable) {
816✔
368
            if ($str instanceof RawSql) {
5✔
369
                return $str->__toString();
4✔
370
            }
371

372
            $str = (string) $str;
1✔
373
        }
374

375
        if (is_string($str)) {
816✔
376
            return pg_escape_literal($this->connID, $str);
815✔
377
        }
378

379
        if (is_bool($str)) {
797✔
380
            return $str ? 'TRUE' : 'FALSE';
687✔
381
        }
382

383
        return parent::escape($str);
797✔
384
    }
385

386
    /**
387
     * Platform-dependant string escape
388
     */
389
    protected function _escapeString(string $str): string
390
    {
391
        if (! $this->connID) {
4✔
UNCOV
392
            $this->initialize();
×
393
        }
394

395
        return pg_escape_string($this->connID, $str);
4✔
396
    }
397

398
    /**
399
     * Generates the SQL for listing tables in a platform-dependent manner.
400
     *
401
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
402
     */
403
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
404
    {
405
        $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'";
758✔
406

407
        if ($tableName !== null) {
758✔
408
            return $sql . ' AND "table_name" LIKE ' . $this->escape($tableName);
757✔
409
        }
410

411
        if ($prefixLimit && $this->DBPrefix !== '') {
61✔
UNCOV
412
            return $sql . ' AND "table_name" LIKE \''
×
UNCOV
413
                . $this->escapeLikeString($this->DBPrefix) . "%' "
×
UNCOV
414
                . sprintf($this->likeEscapeStr, $this->likeEscapeChar);
×
415
        }
416

417
        return $sql;
61✔
418
    }
419

420
    /**
421
     * Generates a platform-specific query string so that the column names can be fetched.
422
     *
423
     * @param string|TableName $table
424
     */
425
    protected function _listColumns($table = ''): string
426
    {
427
        if ($table instanceof TableName) {
8✔
428
            $tableName = $this->escape($table->getActualTableName());
2✔
429
        } else {
430
            $tableName = $this->escape($this->DBPrefix . strtolower($table));
6✔
431
        }
432

433
        return 'SELECT "column_name"
8✔
434
                        FROM "information_schema"."columns"
435
                        WHERE LOWER("table_name") = ' . $tableName
8✔
436
                . ' ORDER BY "ordinal_position"';
8✔
437
    }
438

439
    /**
440
     * Returns an array of objects with field data
441
     *
442
     * @return list<stdClass>
443
     *
444
     * @throws DatabaseException
445
     */
446
    protected function _fieldData(string $table): array
447
    {
448
        $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default",  "is_nullable"
30✔
449
            FROM "information_schema"."columns"
450
            WHERE LOWER("table_name") = '
30✔
451
                . $this->escape(strtolower($table))
30✔
452
                . ' ORDER BY "ordinal_position"';
30✔
453

454
        if (($query = $this->query($sql)) === false) {
30✔
UNCOV
455
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
456
        }
457
        $query = $query->getResultObject();
30✔
458

459
        $retVal = [];
30✔
460

461
        for ($i = 0, $c = count($query); $i < $c; $i++) {
30✔
462
            $retVal[$i] = new stdClass();
30✔
463

464
            $retVal[$i]->name       = $query[$i]->column_name;
30✔
465
            $retVal[$i]->type       = $query[$i]->data_type;
30✔
466
            $retVal[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
30✔
467
            $retVal[$i]->nullable   = $query[$i]->is_nullable === 'YES';
30✔
468
            $retVal[$i]->default    = $query[$i]->column_default;
30✔
469
        }
470

471
        return $retVal;
30✔
472
    }
473

474
    /**
475
     * Returns an array of objects with index data
476
     *
477
     * @return array<string, stdClass>
478
     *
479
     * @throws DatabaseException
480
     */
481
    protected function _indexData(string $table): array
482
    {
483
        $sql = 'SELECT "indexname", "indexdef"
20✔
484
                        FROM "pg_indexes"
485
                        WHERE LOWER("tablename") = ' . $this->escape(strtolower($table)) . '
20✔
486
                        AND "schemaname" = ' . $this->escape('public');
20✔
487

488
        if (($query = $this->query($sql)) === false) {
20✔
UNCOV
489
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
490
        }
491
        $query = $query->getResultObject();
20✔
492

493
        $retVal = [];
20✔
494

495
        foreach ($query as $row) {
20✔
496
            $obj         = new stdClass();
19✔
497
            $obj->name   = $row->indexname;
19✔
498
            $_fields     = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef)));
19✔
499
            $obj->fields = array_map(trim(...), $_fields);
19✔
500

501
            if (str_starts_with($row->indexdef, 'CREATE UNIQUE INDEX pk')) {
19✔
502
                $obj->type = 'PRIMARY';
15✔
503
            } else {
504
                $obj->type = (str_starts_with($row->indexdef, 'CREATE UNIQUE')) ? 'UNIQUE' : 'INDEX';
16✔
505
            }
506

507
            $retVal[$obj->name] = $obj;
19✔
508
        }
509

510
        return $retVal;
20✔
511
    }
512

513
    /**
514
     * Returns an array of objects with Foreign key data
515
     *
516
     * @return array<string, stdClass>
517
     *
518
     * @throws DatabaseException
519
     */
520
    protected function _foreignKeyData(string $table): array
521
    {
522
        $sql = 'SELECT c.constraint_name,
5✔
523
                x.table_name,
524
                x.column_name,
525
                y.table_name as foreign_table_name,
526
                y.column_name as foreign_column_name,
527
                c.delete_rule,
528
                c.update_rule,
529
                c.match_option
530
                FROM information_schema.referential_constraints c
531
                JOIN information_schema.key_column_usage x
532
                    on x.constraint_name = c.constraint_name
533
                JOIN information_schema.key_column_usage y
534
                    on y.ordinal_position = x.position_in_unique_constraint
535
                    and y.constraint_name = c.unique_constraint_name
536
                WHERE x.table_name = ' . $this->escape($table) .
5✔
537
                'order by c.constraint_name, x.ordinal_position';
5✔
538

539
        if (($query = $this->query($sql)) === false) {
5✔
UNCOV
540
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
541
        }
542

543
        $query   = $query->getResultObject();
5✔
544
        $indexes = [];
5✔
545

546
        foreach ($query as $row) {
5✔
547
            $indexes[$row->constraint_name]['constraint_name']       = $row->constraint_name;
4✔
548
            $indexes[$row->constraint_name]['table_name']            = $table;
4✔
549
            $indexes[$row->constraint_name]['column_name'][]         = $row->column_name;
4✔
550
            $indexes[$row->constraint_name]['foreign_table_name']    = $row->foreign_table_name;
4✔
551
            $indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
4✔
552
            $indexes[$row->constraint_name]['on_delete']             = $row->delete_rule;
4✔
553
            $indexes[$row->constraint_name]['on_update']             = $row->update_rule;
4✔
554
            $indexes[$row->constraint_name]['match']                 = $row->match_option;
4✔
555
        }
556

557
        return $this->foreignKeyDataToObjects($indexes);
5✔
558
    }
559

560
    /**
561
     * Returns platform-specific SQL to disable foreign key checks.
562
     *
563
     * @return string
564
     */
565
    protected function _disableForeignKeyChecks()
566
    {
567
        return 'SET CONSTRAINTS ALL DEFERRED';
763✔
568
    }
569

570
    /**
571
     * Returns platform-specific SQL to enable foreign key checks.
572
     *
573
     * @return string
574
     */
575
    protected function _enableForeignKeyChecks()
576
    {
577
        return 'SET CONSTRAINTS ALL IMMEDIATE;';
763✔
578
    }
579

580
    /**
581
     * Returns the last error code and message.
582
     * Must return this format: ['code' => string|int, 'message' => string]
583
     * intval(code) === 0 means "no error".
584
     *
585
     * @return array{code: int|string|null, message: string|null}
586
     */
587
    public function error(): array
588
    {
589
        if ($this->lastFailedResult instanceof PgSqlResult) {
3✔
590
            return [
1✔
591
                'code'    => (string) pg_result_error_field($this->lastFailedResult, PGSQL_DIAG_SQLSTATE),
1✔
592
                'message' => (string) pg_result_error($this->lastFailedResult),
1✔
593
            ];
1✔
594
        }
595

596
        // Fallback for connection-level errors: no SQLSTATE outside a result resource.
597
        $message = pg_last_error($this->connID);
2✔
598

599
        return [
2✔
600
            'code'    => $message !== '' ? '08006' : 0,
2✔
601
            'message' => $message,
2✔
602
        ];
2✔
603
    }
604

605
    /**
606
     * @return int|string
607
     */
608
    public function insertID()
609
    {
610
        $v = pg_version($this->connID);
88✔
611
        // 'server' key is only available since PostgreSQL 7.4
612
        $v = explode(' ', $v['server'])[0] ?? 0;
88✔
613

614
        $table  = func_num_args() > 0 ? func_get_arg(0) : null;
88✔
615
        $column = func_num_args() > 1 ? func_get_arg(1) : null;
88✔
616

617
        if ($table === null && $v >= '8.1') {
88✔
618
            $sql = 'SELECT LASTVAL() AS ins_id';
88✔
619
        } elseif ($table !== null) {
×
UNCOV
620
            if ($column !== null && $v >= '8.0') {
×
UNCOV
621
                $sql   = "SELECT pg_get_serial_sequence('{$table}', '{$column}') AS seq";
×
UNCOV
622
                $query = $this->query($sql);
×
UNCOV
623
                $query = $query->getRow();
×
UNCOV
624
                $seq   = $query->seq;
×
625
            } else {
626
                // seq_name passed in table parameter
UNCOV
627
                $seq = $table;
×
628
            }
629

UNCOV
630
            $sql = "SELECT CURRVAL('{$seq}') AS ins_id";
×
631
        } else {
UNCOV
632
            return pg_last_oid($this->resultID);
×
633
        }
634

635
        $query = $this->query($sql);
88✔
636
        $query = $query->getRow();
88✔
637

638
        return (int) $query->ins_id;
88✔
639
    }
640

641
    /**
642
     * Build a DSN from the provided parameters
643
     *
644
     * @return void
645
     */
646
    protected function buildDSN()
647
    {
648
        if ($this->DSN !== '') {
58✔
UNCOV
649
            $this->DSN = '';
×
650
        }
651

652
        // If UNIX sockets are used, we shouldn't set a port
653
        if (str_contains($this->hostname, '/')) {
58✔
UNCOV
654
            $this->port = '';
×
655
        }
656

657
        if ($this->hostname !== '') {
58✔
658
            $this->DSN = "host={$this->hostname} ";
58✔
659
        }
660

661
        // ctype_digit only accepts strings
662
        $port = (string) $this->port;
58✔
663

664
        if ($port !== '' && ctype_digit($port)) {
58✔
665
            $this->DSN .= "port={$port} ";
58✔
666
        }
667

668
        if ($this->username !== '') {
58✔
669
            $this->DSN .= "user={$this->username} ";
58✔
670

671
            // An empty password is valid!
672
            // password must be set to null to ignore it.
673
            if ($this->password !== null) {
58✔
674
                $this->DSN .= "password='{$this->password}' ";
58✔
675
            }
676
        }
677

678
        if ($this->database !== '') {
58✔
679
            $this->DSN .= "dbname={$this->database} ";
58✔
680
        }
681

682
        // We don't have these options as elements in our standard configuration
683
        // array, but they might be set by parse_url() if the configuration was
684
        // provided via string> Example:
685
        //
686
        // Postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1
687
        foreach (['connect_timeout', 'options', 'sslmode', 'service'] as $key) {
58✔
688
            if (isset($this->{$key}) && is_string($this->{$key}) && $this->{$key} !== '') {
58✔
689
                $this->DSN .= "{$key}='{$this->{$key}}' ";
1✔
690
            }
691
        }
692

693
        $this->DSN = rtrim($this->DSN);
58✔
694
    }
695

696
    /**
697
     * Set client encoding
698
     */
699
    protected function setClientEncoding(string $charset): bool
700
    {
701
        return pg_set_client_encoding($this->connID, $charset) === 0;
59✔
702
    }
703

704
    /**
705
     * Executes a transaction control command (BEGIN, COMMIT, ROLLBACK).
706
     *
707
     * Captures the result resource so SQLSTATE is available via error(),
708
     * resets $lastFailedResult to prevent stale state, and wraps any
709
     * ErrorException into a DatabaseException for consistent error semantics.
710
     */
711
    private function executeTransactionCommand(string $sql): bool
712
    {
713
        $this->lastFailedResult = null;
44✔
714

715
        try {
716
            $result = pg_query($this->connID, $sql);
44✔
717
        } catch (ErrorException $e) {
×
UNCOV
718
            $this->lastException = new DatabaseException($e->getMessage(), 0, $e);
×
719

720
            return false;
×
721
        }
722

723
        if ($result === false) {
44✔
724
            // Connection-level failure: no result resource, SQLSTATE unavailable.
725
            // error() will fall back to pg_last_error() with code '08006'.
UNCOV
726
            return false;
×
727
        }
728

729
        if (pg_result_status($result) === PGSQL_FATAL_ERROR) {
44✔
UNCOV
730
            $this->lastFailedResult = $result;
×
731

UNCOV
732
            $sqlstate            = (string) pg_result_error_field($result, PGSQL_DIAG_SQLSTATE);
×
UNCOV
733
            $message             = (string) pg_result_error($result);
×
UNCOV
734
            $this->lastException = new DatabaseException($message, $sqlstate);
×
735

UNCOV
736
            return false;
×
737
        }
738

739
        return true;
44✔
740
    }
741

742
    /**
743
     * Begin Transaction
744
     */
745
    protected function _transBegin(): bool
746
    {
747
        return $this->executeTransactionCommand('BEGIN');
44✔
748
    }
749

750
    /**
751
     * Commit Transaction
752
     */
753
    protected function _transCommit(): bool
754
    {
755
        return $this->executeTransactionCommand('COMMIT');
16✔
756
    }
757

758
    /**
759
     * Rollback Transaction
760
     */
761
    protected function _transRollback(): bool
762
    {
763
        return $this->executeTransactionCommand('ROLLBACK');
33✔
764
    }
765
}
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