Browse Source

Implementation of native PostgreSQL interface

Changes to the Database class were required to avoid outputting booleans
J. King 3 months ago
parent
commit
2bebdd44cf

+ 4
- 4
lib/Database.php View File

@@ -384,9 +384,9 @@ class Database {
384 384
                 folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id)
385 385
             ".
386 386
             "SELECT
387
-                ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) as extant,
388
-                not exists(select id from folders where id = coalesce((select dest from target),0)) as valid,
389
-                not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) as available
387
+                case when ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) then 1 else 0 end as extant,
388
+                case when not exists(select id from folders where id = coalesce((select dest from target),0)) then 1 else 0 end as valid,
389
+                case when not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) then 1 else 0 end as available
390 390
             ",
391 391
             "str",
392 392
             "strict int",
@@ -418,7 +418,7 @@ class Database {
418 418
             // make sure that a folder with the same prospective name and parent does not already exist: if the parent is null,
419 419
             // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
420 420
             $parent = $parent ? $parent : null;
421
-            if ($this->db->prepare("SELECT exists(select id from arsse_folders where coalesce(parent,0) = ? and name = ?)", "strict int", "str")->run($parent, $name)->getValue()) {
421
+            if ($this->db->prepare("SELECT count(*) from arsse_folders where coalesce(parent,0) = ? and name = ?", "strict int", "str")->run($parent, $name)->getValue()) {
422 422
                 throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]);
423 423
             }
424 424
             return true;

+ 42
- 0
lib/Db/PostgreSQL/Dispatch.php View File

@@ -0,0 +1,42 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\Db\PostgreSQL;
8
+
9
+use JKingWeb\Arsse\Arsse;
10
+use JKingWeb\Arsse\Conf;
11
+use JKingWeb\Arsse\Db\Exception;
12
+use JKingWeb\Arsse\Db\ExceptionInput;
13
+use JKingWeb\Arsse\Db\ExceptionTimeout;
14
+
15
+trait Dispatch {
16
+    protected function dispatchQuery(string $query, array $params = []) {
17
+        pg_send_query_params($this->db, $query, $params);
18
+        $result = pg_get_result($this->db);
19
+        if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) {
20
+            return $this->buildException($code, pg_result_error($result));
21
+        } else {
22
+            return $result;
23
+        }
24
+    }
25
+
26
+    protected function buildException(string $code, string $msg): array {
27
+        switch ($code) {
28
+            case "22P02":
29
+            case "42804":
30
+                return [ExceptionInput::class, 'engineTypeViolation', $msg];
31
+            case "23000":
32
+            case "23502":
33
+            case "23505":
34
+                return [ExceptionInput::class, "engineConstraintViolation", $msg];
35
+            case "55P03":
36
+            case "57014":
37
+                return [ExceptionTimeout::class, 'general', $msg];
38
+            default:
39
+                return [Exception::class, "engineErrorGeneral", $code.": ".$msg]; // @codeCoverageIgnore
40
+        }
41
+    }
42
+}

+ 34
- 17
lib/Db/PostgreSQL/Driver.php View File

@@ -13,6 +13,9 @@ use JKingWeb\Arsse\Db\ExceptionInput;
13 13
 use JKingWeb\Arsse\Db\ExceptionTimeout;
14 14
 
15 15
 class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
16
+    use Dispatch;
17
+
18
+    protected $db;
16 19
     protected $transStart = 0;
17 20
 
18 21
     public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) {
@@ -156,46 +159,60 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
156 159
     }
157 160
 
158 161
     public function __destruct() {
162
+        if (isset($this->db)) {
163
+            pg_close($this->db);
164
+            unset($this->db);
165
+        }
159 166
     }
160 167
 
161
-    /** @codeCoverageIgnore */
162 168
     public static function driverName(): string {
163 169
         return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name");
164 170
     }
165 171
 
166
-    /** @codeCoverageIgnore */
167 172
     public static function requirementsMet(): bool {
168
-        // stub: native interface is not yet supported
169
-        return false;
173
+        return \extension_loaded("pgsql");
170 174
     }
171 175
 
172
-    /** @codeCoverageIgnore */
173 176
     protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) {
174
-        // stub: native interface is not yet supported
175
-        throw new \Exception;
177
+        $dsn = $this->makeconnectionString(false, $user, $pass, $db, $host, $port, $service);
178
+        set_error_handler(function(int $code, string $msg) {
179
+            $msg = substr($msg, 62);
180
+            throw new Exception("connectionFailure", ["PostgreSQL", $msg]);
181
+        });
182
+        try {
183
+            $this->db = pg_connect($dsn, \PGSQL_CONNECT_FORCE_NEW);
184
+        } finally {
185
+            restore_error_handler();
186
+        }
176 187
     }
177 188
 
178
-    /** @codeCoverageIgnore */
179 189
     protected function getError(): string {
180
-        // stub: native interface is not yet supported
190
+        // stub
181 191
         return "";
182 192
     }
183 193
 
184
-    /** @codeCoverageIgnore */
185 194
     public function exec(string $query): bool {
186
-        // stub: native interface is not yet supported
195
+        pg_send_query($this->db, $query);
196
+        while ($result = pg_get_result($this->db)) {
197
+            if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) {
198
+                list($excClass, $excMsg, $excData) = $this->buildException($code, pg_result_error($result));
199
+                throw new $excClass($excMsg, $excData);
200
+            }
201
+        }
187 202
         return true;
188 203
     }
189 204
 
190
-    /** @codeCoverageIgnore */
191 205
     public function query(string $query): \JKingWeb\Arsse\Db\Result {
192
-        // stub: native interface is not yet supported
193
-        return new ResultEmpty;
206
+        $r = $this->dispatchQuery($query);
207
+        if (is_resource($r)) {
208
+            return new Result($this->db, $r);
209
+        } else {
210
+            list($excClass, $excMsg, $excData) = $r;
211
+            throw new $excClass($excMsg, $excData);
212
+        }
194 213
     }
195 214
 
196
-    /** @codeCoverageIgnore */
197 215
     public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
198
-        // stub: native interface is not yet supported
199
-        return new Statement($this->db, $s, $paramTypes);
216
+        return new Statement($this->db, $query, $paramTypes);
200 217
     }
201 218
 }

+ 3
- 25
lib/Db/PostgreSQL/PDOStatement.php View File

@@ -6,18 +6,9 @@
6 6
 declare(strict_types=1);
7 7
 namespace JKingWeb\Arsse\Db\PostgreSQL;
8 8
 
9
-class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
9
+class PDOStatement extends Statement {
10 10
     use \JKingWeb\Arsse\Db\PDOError;
11 11
 
12
-    const BINDINGS = [
13
-        "integer"   => "bigint",
14
-        "float"     => "decimal",
15
-        "datetime"  => "timestamp(0) without time zone",
16
-        "binary"    => "bytea",
17
-        "string"    => "text",
18
-        "boolean"   => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
19
-    ];
20
-
21 12
     protected $db;
22 13
     protected $st;
23 14
     protected $qOriginal;
@@ -25,7 +16,7 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
25 16
     protected $bindings;
26 17
 
27 18
     public function __construct(\PDO $db, string $query, array $bindings = []) {
28
-        $this->db = $db; // both db and st are the same object due to the logic of the PDOError handler
19
+        $this->db = $db;
29 20
         $this->qOriginal = $query;
30 21
         $this->retypeArray($bindings);
31 22
     }
@@ -53,19 +44,6 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
53 44
         return true;
54 45
     }
55 46
 
56
-    public static function mungeQuery(string $q, array $types, bool $mungeParamMarkers = true): string {
57
-        $q = explode("?", $q);
58
-        $out = "";
59
-        for ($b = 1; $b < sizeof($q); $b++) {
60
-            $a = $b - 1;
61
-            $mark = $mungeParamMarkers ? "\$$b" : "?";
62
-            $type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : "";
63
-            $out .= $q[$a].$mark.$type;
64
-        }
65
-        $out .= array_pop($q);
66
-        return $out;
67
-    }
68
-
69 47
     public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
70 48
         return $this->st->runArray($values);
71 49
     }
@@ -73,6 +51,6 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
73 51
     /** @codeCoverageIgnore */
74 52
     protected function bindValue($value, string $type, int $position): bool {
75 53
         // stub required by abstract parent, but never used
76
-        return $value;
54
+        return true;
77 55
     }
78 56
 }

+ 48
- 0
lib/Db/PostgreSQL/Result.php View File

@@ -0,0 +1,48 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\Db\PostgreSQL;
8
+
9
+use JKingWeb\Arsse\Db\Exception;
10
+
11
+class Result extends \JKingWeb\Arsse\Db\AbstractResult {
12
+    protected $db;
13
+    protected $r;
14
+    protected $cur;
15
+
16
+    // actual public methods
17
+
18
+    public function changes(): int {
19
+        return pg_affected_rows($this->r);
20
+    }
21
+
22
+    public function lastId(): int {
23
+        if ($r = @pg_query($this->db, "SELECT lastval()")) {
24
+            return (int) pg_fetch_result($r, 0, 0);
25
+        } else {
26
+            return 0;
27
+        }
28
+    }
29
+
30
+    // constructor/destructor
31
+
32
+    public function __construct($db, $result) {
33
+        $this->db = $db;
34
+        $this->r = $result;
35
+    }
36
+
37
+    public function __destruct() {
38
+        pg_free_result($this->r);
39
+        unset($this->r, $this->db);
40
+    }
41
+
42
+    // PHP iterator methods
43
+
44
+    public function valid() {
45
+        $this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC);
46
+        return ($this->cur !== false);
47
+    }
48
+}

+ 77
- 0
lib/Db/PostgreSQL/Statement.php View File

@@ -0,0 +1,77 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\Db\PostgreSQL;
8
+
9
+use JKingWeb\Arsse\Db\Exception;
10
+use JKingWeb\Arsse\Db\ExceptionInput;
11
+use JKingWeb\Arsse\Db\ExceptionTimeout;
12
+
13
+class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
14
+    use Dispatch;
15
+
16
+    const BINDINGS = [
17
+        "integer"   => "bigint",
18
+        "float"     => "decimal",
19
+        "datetime"  => "timestamp(0) without time zone",
20
+        "binary"    => "bytea",
21
+        "string"    => "text",
22
+        "boolean"   => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
23
+    ];
24
+
25
+    protected $db;
26
+    protected $in = [];
27
+    protected $qOriginal;
28
+    protected $qMunged;
29
+    protected $bindings;
30
+
31
+    public function __construct($db, string $query, array $bindings = []) {
32
+        $this->db = $db; 
33
+        $this->qOriginal = $query;
34
+        $this->retypeArray($bindings);
35
+    }
36
+
37
+    public function retypeArray(array $bindings, bool $append = false): bool {
38
+        if ($append) {
39
+            return parent::retypeArray($bindings, $append);
40
+        } else {
41
+            $this->bindings = $bindings;
42
+            parent::retypeArray($bindings, $append);
43
+            $this->qMunged = self::mungeQuery($this->qOriginal, $this->types, true);
44
+        }
45
+        return true;
46
+    }
47
+
48
+    public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
49
+        $this->in = [];
50
+        $this->bindValues($values);
51
+        $r = $this->dispatchQuery($this->qMunged, $this->in);
52
+        if (is_resource($r)) {
53
+            return new Result($this->db, $r);
54
+        } else {
55
+            list($excClass, $excMsg, $excData) = $r;
56
+            throw new $excClass($excMsg, $excData);
57
+        }
58
+    }
59
+
60
+    protected function bindValue($value, string $type, int $position): bool {
61
+        $this->in[] = $value;
62
+        return true;
63
+    }
64
+
65
+    protected static function mungeQuery(string $q, array $types, bool $mungeParamMarkers = true): string {
66
+        $q = explode("?", $q);
67
+        $out = "";
68
+        for ($b = 1; $b < sizeof($q); $b++) {
69
+            $a = $b - 1;
70
+            $mark = $mungeParamMarkers ? "\$$b" : "?";
71
+            $type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : "";
72
+            $out .= $q[$a].$mark.$type;
73
+        }
74
+        $out .= array_pop($q);
75
+        return $out;
76
+    }
77
+}

+ 1
- 1
locale/en.php View File

@@ -159,7 +159,7 @@ return [
159 159
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing'            => 'Referenced ID ({id}) in field "{field}" does not exist',
160 160
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing'                 => 'Referenced ID ({id}) in field "{field}" does not exist',
161 161
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence'        => 'Referenced ID ({id}) in field "{field}" creates a circular dependence',
162
-    'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation'       => 'Specified value in field "{0}" already exists',
162
+    'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation'       => 'Specified value in field "{field}" already exists',
163 163
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}',
164 164
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation'       => '{0}',
165 165
     'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general'                 => '{0}',

+ 73
- 0
tests/cases/Db/PostgreSQL/TestCreation.php View File

@@ -0,0 +1,73 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
8
+
9
+use JKingWeb\Arsse\Arsse;
10
+use JKingWeb\Arsse\Db\PostgreSQL\Driver;
11
+
12
+/**
13
+ * @group slow
14
+ * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
15
+class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
16
+    public function setUp() {
17
+        if (!Driver::requirementsMet()) {
18
+            $this->markTestSkipped("PostgreSQL extension not loaded");
19
+        }
20
+    }
21
+    
22
+    /** @dataProvider provideConnectionStrings */
23
+    public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) {
24
+        self::setConf();
25
+        $timeout = (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0);
26
+        $postfix = "application_name='arsse' client_encoding='UTF8' connect_timeout='$timeout'";
27
+        $act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service);
28
+        if ($act==$postfix) {
29
+            $this->assertSame($exp, "");
30
+        } else {
31
+            $test = substr($act, 0, strlen($act) - (strlen($postfix) + 1));
32
+            $check = substr($act, strlen($test) + 1);
33
+            $this->assertSame($postfix, $check);
34
+            $this->assertSame($exp, $test);
35
+        }
36
+    }
37
+
38
+    public function provideConnectionStrings() {
39
+        return [
40
+            [false, "arsse",           "secret",   "arsse",     "",          5432, "",      "dbname='arsse' password='secret' user='arsse'"],
41
+            [false, "arsse",           "p word",   "arsse",     "",          5432, "",      "dbname='arsse' password='p word' user='arsse'"],
42
+            [false, "arsse",           "p'word",   "arsse",     "",          5432, "",      "dbname='arsse' password='p\\'word' user='arsse'"],
43
+            [false, "arsse user",      "secret",   "arsse db",  "",          5432, "",      "dbname='arsse db' password='secret' user='arsse user'"],
44
+            [false, "arsse",           "secret",   "",          "",          5432, "",      "password='secret' user='arsse'"],
45
+            [false, "arsse",           "secret",   "arsse",     "localhost", 5432, "",      "dbname='arsse' host='localhost' password='secret' user='arsse'"],
46
+            [false, "arsse",           "secret",   "arsse",     "",          9999, "",      "dbname='arsse' password='secret' port='9999' user='arsse'"],
47
+            [false, "arsse",           "secret",   "arsse",     "localhost", 9999, "",      "dbname='arsse' host='localhost' password='secret' port='9999' user='arsse'"],
48
+            [false, "arsse",           "secret",   "arsse",     "/socket",   9999, "",      "dbname='arsse' host='/socket' password='secret' user='arsse'"],
49
+            [false, "T'Pau of Vulcan", "",         "",          "",          5432, "",      "user='T\\'Pau of Vulcan'"],
50
+            [false, "T'Pau of Vulcan", "superman", "datumbase", "somehost",  2112, "arsse", "service='arsse'"],
51
+            [true,  "arsse",           "secret",   "arsse",     "",          5432, "",      "dbname='arsse'"],
52
+            [true,  "arsse",           "p word",   "arsse",     "",          5432, "",      "dbname='arsse'"],
53
+            [true,  "arsse",           "p'word",   "arsse",     "",          5432, "",      "dbname='arsse'"],
54
+            [true,  "arsse user",      "secret",   "arsse db",  "",          5432, "",      "dbname='arsse db'"],
55
+            [true,  "arsse",           "secret",   "",          "",          5432, "",      ""],
56
+            [true,  "arsse",           "secret",   "arsse",     "localhost", 5432, "",      "dbname='arsse' host='localhost'"],
57
+            [true,  "arsse",           "secret",   "arsse",     "",          9999, "",      "dbname='arsse' port='9999'"],
58
+            [true,  "arsse",           "secret",   "arsse",     "localhost", 9999, "",      "dbname='arsse' host='localhost' port='9999'"],
59
+            [true,  "arsse",           "secret",   "arsse",     "/socket",   9999, "",      "dbname='arsse' host='/socket'"],
60
+            [true,  "T'Pau of Vulcan", "",         "",          "",          5432, "",      ""],
61
+            [true,  "T'Pau of Vulcan", "superman", "datumbase", "somehost",  2112, "arsse", "service='arsse'"],
62
+        ];
63
+    }
64
+
65
+    public function testFailToConnect() {
66
+        // we cannnot distinguish between different connection failure modes
67
+        self::setConf([
68
+            'dbPostgreSQLPass' => (string) rand(),
69
+        ]);
70
+        $this->assertException("connectionFailure", "Db");
71
+        new Driver;
72
+    }
73
+}

+ 43
- 0
tests/cases/Db/PostgreSQL/TestDatabase.php View File

@@ -0,0 +1,43 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
8
+
9
+/**
10
+ * @group slow
11
+ * @group coverageOptional
12
+ * @covers \JKingWeb\Arsse\Database<extended>
13
+ * @covers \JKingWeb\Arsse\Misc\Query<extended>
14
+ */
15
+class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
16
+    protected static $implementation = "PostgreSQL";
17
+
18
+    protected function nextID(string $table): int {
19
+        return (int) static::$drv->query("SELECT coalesce(last_value, (select max(id) from $table)) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue();
20
+    }
21
+
22
+    public function setUp() {
23
+        parent::setUp();
24
+        $seqList =
25
+            "select 
26
+                replace(substring(column_default, 10), right(column_default, 12), '') as seq, 
27
+                table_name as table, 
28
+                column_name as col 
29
+            from information_schema.columns
30
+                where table_schema = current_schema()
31
+                and table_name like 'arsse_%' 
32
+                and column_default like 'nextval(%'
33
+            ";
34
+        foreach (static::$drv->query($seqList) as $r) {
35
+            $num = (int) static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue();
36
+            if (!$num) {
37
+                continue;
38
+            }
39
+            $num++;
40
+            static::$drv->exec("ALTER SEQUENCE {$r['seq']} RESTART WITH $num");
41
+        }
42
+    }
43
+}

+ 57
- 0
tests/cases/Db/PostgreSQL/TestDriver.php View File

@@ -0,0 +1,57 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
8
+
9
+/**
10
+ * @group slow
11
+ * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended>  */
12
+class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
13
+    protected static $implementation = "PostgreSQL";
14
+    protected $create = "CREATE TABLE arsse_test(id bigserial primary key)";
15
+    protected $lock = ["BEGIN", "LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"];
16
+    protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'";
17
+
18
+    public function tearDown() {
19
+        try {
20
+            $this->drv->exec("ROLLBACK");
21
+        } catch (\Throwable $e) {
22
+        }
23
+        parent::tearDown();
24
+    }
25
+
26
+    public static function tearDownAfterClass() {
27
+        if (static::$interface) {
28
+            (static::$dbInfo->razeFunction)(static::$interface);
29
+            @pg_close(static::$interface);
30
+            static::$interface = null;
31
+        }
32
+        parent::tearDownAfterClass();
33
+    }
34
+
35
+    protected function exec($q): bool {
36
+        $q = (!is_array($q)) ? [$q] : $q;
37
+        foreach ($q as $query) {
38
+            set_error_handler(function($code, $msg) {
39
+                throw new \Exception($msg);
40
+            });
41
+            try {
42
+                pg_query(static::$interface, $query);
43
+            } finally {
44
+                restore_error_handler();
45
+            }
46
+        }
47
+        return true;
48
+    }
49
+
50
+    protected function query(string $q) {
51
+        if ($r = pg_query_params(static::$interface, $q, [])) {
52
+            return pg_fetch_result($r, 0, 0);
53
+        } else {
54
+            return;
55
+        }
56
+    }
57
+}

+ 33
- 0
tests/cases/Db/PostgreSQL/TestResult.php View File

@@ -0,0 +1,33 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
8
+
9
+use JKingWeb\Arsse\Test\DatabaseInformation;
10
+
11
+/**
12
+ * @group slow
13
+ * @covers \JKingWeb\Arsse\Db\PostgreSQL\Result<extended>
14
+ */
15
+class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
16
+    protected static $implementation = "PostgreSQL";
17
+    protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)";
18
+    protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)";
19
+
20
+    protected function makeResult(string $q): array {
21
+        $set = pg_query(static::$interface, $q);
22
+        return [static::$interface, $set];
23
+    }
24
+
25
+    public static function tearDownAfterClass() {
26
+        if (static::$interface) {
27
+            (static::$dbInfo->razeFunction)(static::$interface);
28
+            @pg_close(static::$interface);
29
+            static::$interface = null;
30
+        }
31
+        parent::tearDownAfterClass();
32
+    }
33
+}

+ 41
- 0
tests/cases/Db/PostgreSQL/TestStatement.php View File

@@ -0,0 +1,41 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
8
+
9
+/**
10
+ * @group slow
11
+ * @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement<extended> */
12
+class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
13
+    protected static $implementation = "PostgreSQL";
14
+
15
+    protected function makeStatement(string $q, array $types = []): array {
16
+        return [static::$interface, $q, $types];
17
+    }
18
+
19
+    protected function decorateTypeSyntax(string $value, string $type): string {
20
+        switch ($type) {
21
+            case "float":
22
+                return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'";
23
+            case "string":
24
+                if (preg_match("<^char\((\d+)\)$>", $value, $match)) {
25
+                    return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'";
26
+                }
27
+                return $value;
28
+            default:
29
+                return $value;
30
+        }
31
+    }
32
+
33
+    public static function tearDownAfterClass() {
34
+        if (static::$interface) {
35
+            (static::$dbInfo->razeFunction)(static::$interface);
36
+            @pg_close(static::$interface);
37
+            static::$interface = null;
38
+        }
39
+        parent::tearDownAfterClass();
40
+    }
41
+}

+ 16
- 0
tests/cases/Db/PostgreSQL/TestUpdate.php View File

@@ -0,0 +1,16 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
8
+
9
+/**
10
+ * @group slow
11
+ * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
12
+class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
13
+    protected static $implementation = "PostgreSQL";
14
+    protected static $minimal1 = "CREATE TABLE arsse_meta(key text primary key, value text); INSERT INTO arsse_meta(key,value) values('schema_version','1');";
15
+    protected static $minimal2 = "UPDATE arsse_meta set value = '2' where key = 'schema_version';";
16
+}

+ 6
- 0
tests/cases/Db/PostgreSQLPDO/TestCreation.php View File

@@ -13,6 +13,12 @@ use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver;
13 13
  * @group slow
14 14
  * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> */
15 15
 class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
16
+    public function setUp() {
17
+        if (!Driver::requirementsMet()) {
18
+            $this->markTestSkipped("PDO-PostgreSQL extension not loaded");
19
+        }
20
+    }
21
+
16 22
     /** @dataProvider provideConnectionStrings */
17 23
     public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) {
18 24
         self::setConf();

+ 1
- 0
tests/cases/Db/PostgreSQLPDO/TestDatabase.php View File

@@ -8,6 +8,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
8 8
 
9 9
 /**
10 10
  * @group slow
11
+ * @group optional
11 12
  * @group coverageOptional
12 13
  * @covers \JKingWeb\Arsse\Database<extended>
13 14
  * @covers \JKingWeb\Arsse\Misc\Query<extended>

+ 1
- 0
tests/cases/Db/SQLite3/TestDatabase.php View File

@@ -7,6 +7,7 @@ declare(strict_types=1);
7 7
 namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
8 8
 
9 9
 /**
10
+ * @group optional
10 11
  * @covers \JKingWeb\Arsse\Database<extended>
11 12
  * @covers \JKingWeb\Arsse\Misc\Query<extended>
12 13
  */

+ 67
- 33
tests/lib/DatabaseInformation.php View File

@@ -116,7 +116,50 @@ class DatabaseInformation {
116 116
             } elseif ($db instanceof \PDO) {
117 117
                 return $db->query($listObjects)->fetchAll(\PDO::FETCH_ASSOC);
118 118
             } else {
119
-                throw \Exception("Native PostgreSQL interface not implemented");
119
+                $r = @pg_query($db, $listObjects);
120
+                $out = $r ? pg_fetch_all($r) : false;
121
+                return $out ? $out : [];
122
+            }
123
+        };
124
+        $pgExecFunction = function($db, $q) {
125
+            if ($db instanceof Driver) {
126
+                $db->exec($q);
127
+            } elseif ($db instanceof \PDO) {
128
+                $db->exec($q);
129
+            } else {
130
+                pg_query($db, $q);
131
+            }
132
+        };
133
+        $pgTruncateFunction = function($db, array $afterStatements = []) use ($pgObjectList, $pgExecFunction) {
134
+            // rollback any pending transaction
135
+            try {
136
+                @$pgExecFunction($db, "ROLLBACK");
137
+            } catch (\Throwable $e) {
138
+            }
139
+            foreach ($pgObjectList($db) as $obj) {
140
+                if ($obj['type'] != "TABLE") {
141
+                    continue;
142
+                } elseif ($obj['name'] == "arsse_meta") {
143
+                    $pgExecFunction($db, "DELETE FROM {$obj['name']} where key <> 'schema_version'");
144
+                } else {
145
+                    $pgExecFunction($db, "TRUNCATE TABLE {$obj['name']} restart identity cascade");
146
+                }
147
+            }
148
+            foreach ($afterStatements as $st) {
149
+                $pgExecFunction($db, $st);
150
+            }
151
+        };
152
+        $pgRazeFunction = function($db, array $afterStatements = []) use ($pgObjectList, $pgExecFunction) {
153
+            // rollback any pending transaction
154
+            try {
155
+                $pgExecFunction($db, "ROLLBACK");
156
+            } catch (\Throwable $e) {
157
+            }
158
+            foreach ($pgObjectList($db) as $obj) {
159
+                $pgExecFunction($db, "DROP {$obj['type']} IF EXISTS {$obj['name']} cascade");
160
+            }
161
+            foreach ($afterStatements as $st) {
162
+                $pgExecFunction($db, $st);
120 163
             }
121 164
         };
122 165
         return [
@@ -158,6 +201,27 @@ class DatabaseInformation {
158 201
                 'truncateFunction' => $sqlite3TruncateFunction,
159 202
                 'razeFunction' => $sqlite3RazeFunction,
160 203
             ],
204
+            'PostgreSQL' => [
205
+                'pdo' => false,
206
+                'backend' => "PostgreSQL",
207
+                'statementClass' => \JKingWeb\Arsse\Db\PostgreSQL\Statement::class,
208
+                'resultClass' => \JKingWeb\Arsse\Db\PostgreSQL\Result::class,
209
+                'driverClass' => \JKingWeb\Arsse\Db\PostgreSQL\Driver::class,
210
+                'stringOutput' => true,
211
+                'interfaceConstructor' => function() {
212
+                    $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(false, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, "");
213
+                    if ($d = @pg_connect($connString, \PGSQL_CONNECT_FORCE_NEW)) {
214
+                        foreach (\JKingWeb\Arsse\Db\PostgreSQL\Driver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) {
215
+                            pg_query($d, $q);
216
+                        }
217
+                        return $d;
218
+                    } else {
219
+                        return;
220
+                    }
221
+                },
222
+                'truncateFunction' => $pgTruncateFunction,
223
+                'razeFunction' => $pgRazeFunction,
224
+            ],
161 225
             'PDO PostgreSQL' => [
162 226
                 'pdo' => true,
163 227
                 'backend' => "PostgreSQL",
@@ -177,38 +241,8 @@ class DatabaseInformation {
177 241
                     }
178 242
                     return $d;
179 243
                 },
180
-                'truncateFunction' => function($db, array $afterStatements = []) use ($pgObjectList) {
181
-                    // rollback any pending transaction
182
-                    try {
183
-                        $db->exec("ROLLBACK");
184
-                    } catch (\Throwable $e) {
185
-                    }
186
-                    foreach ($pgObjectList($db) as $obj) {
187
-                        if ($obj['type'] != "TABLE") {
188
-                            continue;
189
-                        } elseif ($obj['name'] == "arsse_meta") {
190
-                            $db->exec("DELETE FROM {$obj['name']} where key <> 'schema_version'");
191
-                        } else {
192
-                            $db->exec("TRUNCATE TABLE {$obj['name']} restart identity cascade");
193
-                        }
194
-                    }
195
-                    foreach ($afterStatements as $st) {
196
-                        $db->exec($st);
197
-                    }
198
-                },
199
-                'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) {
200
-                    // rollback any pending transaction
201
-                    try {
202
-                        $db->exec("ROLLBACK");
203
-                    } catch (\Throwable $e) {
204
-                    }
205
-                    foreach ($pgObjectList($db) as $obj) {
206
-                        $db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade");
207
-                    }
208
-                    foreach ($afterStatements as $st) {
209
-                        $db->exec($st);
210
-                    }
211
-                },
244
+                'truncateFunction' => $pgTruncateFunction,
245
+                'razeFunction' => $pgRazeFunction,
212 246
             ],
213 247
         ];
214 248
     }

Loading…
Cancel
Save