Commit inicial

This commit is contained in:
2024-08-02 12:00:57 -06:00
commit 4489b65e04
246 changed files with 83097 additions and 0 deletions

View File

@@ -0,0 +1 @@
/vendor/

View File

@@ -0,0 +1,16 @@
language: php
php:
- 5.4
- 5.6
- 7
- 7.1
- 7.2
notifications:
email: false
dist: trusty
addons:
postgresql: "9.6"
before_script:
- psql -c 'CREATE DATABASE test;' -U postgres
- composer install
script: composer test && composer lint

View File

@@ -0,0 +1,10 @@
PHP wrapper class for PDO-based interaction with PostgreSQL databases
Copyright (C) 2015 David Joseph Guzsik
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,43 @@
# PostgresDb [![Build Status](https://travis-ci.org/SeinopSys/PHP-PostgreSQL-Database-Class.svg)](https://travis-ci.org/SeinopSys/PHP-PostgreSQL-Database-Class) [![Latest Stable Version](https://poser.pugx.org/seinopsys/postgresql-database-class/v/stable)](https://packagist.org/packages/seinopsys/postgresql-database-class) [![Total Downloads](https://poser.pugx.org/seinopsys/postgresql-database-class/downloads)](https://packagist.org/packages/seinopsys/postgresql-database-class) [![License](https://poser.pugx.org/seinopsys/postgresql-database-class/license)](https://packagist.org/packages/seinopsys/postgresql-database-class)
This project is a PostgreSQL version of [ThingEngineer](https://github.com/ThingEngineer)'s [MysqliDb Class](https://github.com/ThingEngineer/PHP-MySQLi-Database-Class), that supports the basic functionality and syntax provided by said class, tailored specifically to PostgreSQL.
## Installation
This class requires PHP 5.4+ or 7+ to work. You can either place the `src/PostgresDb.php` in your project and require/include it, or use [Composer](https://getcomposer.org) (strongly recommended)
composer require seinopsys/postgresql-database-class:^3.0
## Usage
```php
$db = new \SeinopSys\PostgresDb($database_name, $host, $username, $password);
```
For a more in-depth guide see [USAGE.md](USAGE.md)
## Upgrading from `2.x`
1. **Removed deprecated methods**
These methods were deprecated in version `2.x` and have been removed in `3.x`. Use the renamed variants as indicated below:
| `2.x` | `3.x` |
|-------|-------|
|`$db->rawQuery(…);`|`$db->query(…);`|
|`$db->rawQuerySingle(…);`|`$db->querySingle(…);`|
|`$db->pdo();`|`$db->getConnection();`|
2. **Namespace change**
As of `3.x` - to comply fully with the PSR-2 coding standard - the class now resides in the `SeinopSys` namespace. Here's a handy table to show what you need to change and how:
| `2.x` | `3.x` |
|-------|-------|
|`$db = new PostgresDb(…);`|`$db = new \SeinopSys\PostgresDb(…);`|
|`$db = new \PostgresDb(…);`|`$db = new \SeinopSys\PostgresDb(…);`|
|<pre>use \PostgresDb;<br><br>$db = new PostgresDb(…);</pre>|<pre>use \SeinopSys\PostgresDb;<br><br>$db = new PostgresDb(…);</pre>|
3. **Internal code structure changes**
As of `3.x` all `private`/`protected` methods and properties have dropped the `_` prefix, so be sure to update your wrapper class (if you use one).

View File

@@ -0,0 +1,420 @@
## Usage
For the examples below, the following `users` table structure is used:
| id | name | gender |
|----|--------|--------|
| 1 | Sam | m |
| 2 | Alex | f |
| 3 | Daniel | m |
### Connecting to a database
The class works with Composer's autoloading, so if you install through it you just need to require the `vendor/autoload.php` file somewhere in your project. It's defined in the `SesinopSys` namespace, so if you're using it from within a namespace, refer to it as `\SeinopSys\PostgresDb` or add `use \SeinopSys\PostgresDb;` at the top of the file so you can drop the namespace afterwards.
If you decided to go the simple include route then you can include the `src/PostgresDb.php` file and use it pretty much the same way.
To start using the class you need to create an instance with the following parameters.
```php
use \SeinopSys\PostgresDb;
$db = new PostgresDb($database_name, $host, $username, $password);
```
Due to the way these parameters are mapped to the internal connection call within PHP these values must not contain spaces.
Initializing the class does not immediately create a connection. To force a connection attempt, call:
```php
$db->getConnection();
```
This will return the internal PDO object (and create it if it doesn't exist yet) used by the class and simultaneously attempts to connect to the database. Initial connection errors can be caught by using `try…catch` but by default the script uses `PDO::ERRMODE_WARNING` for further errors.
If you'd prefer exceptions to be thrown instead, or if you like to live dangerously and want to silence all errors, you can use the chainable `setPDOErrmode()` method by passing any of the `PDO::ERRMODE_*` constants both before and after the connection has been made.
```php
// Before connection
$db->setPDOErrmode(PDO::ERRMODE_EXCEPTION)->getConnection();
// After connection
$db->getConnection();
$db->setPDOErrmode(PDO::ERRMODE_SILENT)->get()->setPDOErrmode(PDO::ERRMODE_WARNING);
```
If you need the value later for whatever reason you can read it out using `getPDOErrmode()`.
If you happen to have an existing PDO connection lying around you can also use it with this class, like so:
```php
require "PostgresDb.php";
$db = new PostgresDb();
$db->setConnection($existing_connection);
```
Any future method calls on the `$db` object will act on the existing connection instead of creating a new instance. Only use a different connection with this class if you know what you're doing. Manipulating the same PDO object with different libraries can result in strange behavior, so be careful.
### Selecting [`get()`, `getOne()`]
Returns an array containing each result as an array of its own. Returns `false` if no row is found.
```php
// SELECT * FROM users
$db->get('users');
// SELECT * FROM users LIMIT 1
$db->get('users', 1);
// SELECT id, name FROM users LIMIT 1
$db->get('users', 1, 'id, name');
// SELECT id, name FROM users LIMIT 2 OFFSET 1
$db->get('users', [1,2], 'id, name');
```
Return format:
```php
[
[
'id' => 2,
'name' => 'Alex'
],
[
'id' => 3,
'name' => 'Daniel'
]
]
```
Alternatively, you can use `getOne` to only select a single row. Returns `false` if no row is found.
```php
// SELECT * FROM users LIMIT 1
$db->getOne('users');
// SELECT id, name FROM users LIMIT 1
$db->getOne('users', 'id, name');
```
Return format:
```php
[
'id' => 1,
'name' => 'Sam'
]
```
### Where [`where()`]
```php
// SELECT * FROM users WHERE "id" = 1 LIMIT 1
$db->where('id', 1)->get('users');
// SELECT * FROM users LIMIT 1 WHERE "id" != 1
$db->where('"id" != 1')->getOne('users');
// SELECT * FROM users LIMIT 1 WHERE "id" != 1
$db->where('id', 1, '!=')->getOne('users');
// SELECT * FROM users LIMIT 1 WHERE "id" IN (1, 3)
$db->where('id', [1, 3])->getOne('users');
// SELECT * FROM users LIMIT 1 WHERE "id" NOT IN (1, 3)
$db->where('id', [1, 3], '!=')->getOne('users');
```
### Join [`join()`]
```php
// SELECT * FROM users u LEFT JOIN "messages" m ON m.user = u.id
$db->join('messages m', 'm.user = u.id', 'LEFT')->get('users u');
// SELECT * FROM users u INNER JOIN "messages" m ON u.id = m.user
$db->join('messages m', 'u.id = m.user', 'INNER')->get('users u');
```
### Ordering [`orderBy()`, `orderByLiteral()`]
Complex `ORDER BY` statements can be passed to `orderByLiteral` which will just use the value you provide as it is. you can use the built-in constant to order the results randomly.
```php
// SELECT * FROM users ORDER BY "name" DESC
$db->orderBy('name')->get('users');
// SELECT * FROM users ORDER BY "name" ASC
$db->orderBy('name','ASC')->get('users');
// SELECT * FROM users ORDER BY rand()
$db->orderBy(PostgresDb::ORDERBY_RAND)->get('users');
// SELECT * FROM users ORDER BY CASE WHEN "name" IS NULL THEN 1 ELSE 0 END DESC, "name" ASC
$db
->orderByLiteral('CASE WHEN "name" IS NULL THEN 1 ELSE 0 END')
->orderBy('name','ASC')
->get('users');
```
### Grouping [`groupBy()`]
```php
// SELECT * FROM "users" GROUP BY "gender"
$db->groupBy('gender')->get('users');
// SELECT * FROM "users" GROUP BY "gender", "othercol"
$db->groupBy('gender')->groupBy('othercol')->get('users');
```
### Inserting [`insert()`]
Returns `false` on failure and `true` if successful.
```php
// INSERT INTO users ("name", gender) VALUES ('Joe', 'm')
$db->insert('users', ['name' => 'Joe', 'gender' => 'm']);
```
| id | name | gender |
|-------|---------|--------|
| 1 | Sam | m |
| 2 | Alex | f |
| 3 | Daniel | m |
| **4** | **Joe** | **m** |
You can also ask for the values of inserted columns, in which case it'll return those instead of `true` if the insert is successful. This is especially useful when you need the value of an index that's generated on the fly by Postgres.
When returning a single column its value is returned by the method, and when multiple columns are specified the return value is an associative array with the keys being the columns and the values being, well, the values.
```php
// INSERT INTO users ("name") VALUES ('Joe') RETURNING id
$id = $db->insert('users', ['name' => 'Joe'], 'id');
// $id === 4
```
```php
// INSERT INTO users ("name") VALUES ('Joe') RETURNING id, "name"
$return = $db->insert('users', ['name' => 'Joe'], ['id', 'name']);
// or $db->insert('users', ['name' => 'Joe'], 'id, name');
// $return === ['id' => 4, 'name' => 'Joe']
```
### Update [`update()`]
Returns `false` on failure and `true` if successful.
```php
// UPDATE users SET "name" = 'Dave' WHERE "id" = 1
$db->where('id', 1)->update('users', ['name' => 'Dave']);
```
| id | name | gender |
|----|----------|--------|
| 1 | **Dave** | m |
| 2 | Alex | f |
| 3 | Daniel | m |
| 4 | Joe | m |
### Delete [`delete()`]
Returns `false` on failure and `true` if successful. Cannot be used with `groupBy()`, `join()` and `orderBy()`.
```php
// DELETE FROM users WHERE "id" = 3
$success = $db->where('id', 3)->delete('users');
// $success === true
```
A second argument can optionally be passed to return columns from the deleted row, similar to `insert()`.
```php
// DELETE FROM users WHERE "id" = 3 RETURNING gender
$return = $db->where('id', 3)->delete('users', 'gender');
// $return === 'm'
```
```php
// DELETE FROM users WHERE "id" = 3 RETURNING "name", gender
$return = $db->where('id', 3)->delete('users', ['name', 'gender']);
// or $db->where('id', 3)->delete('users', 'name, gender');
// $return === ['name' => 'Daniel', 'gender' => 'm']
```
| id | name | gender |
|----|--------|--------|
| 1 | Dave | m |
| 2 | Alex | f |
| 4 | Joe | m |
### Raw SQL queries [`query()`, `querySingle()`]
Executes the query exactly* as passed, for when it is far too complex to use the built-in methods, or when a built-in method does not exist for what you want to achieve. Return value format matches `get()`'s.
```php
// Query string only
$db->query('SELECT * FROM users WHERE id = 4');
// Using prepared statement parameters
$id = 4;
$db->query('SELECT * FROM users WHERE id = ? && name = ?', [4, 'Joe']);
$db->query('SELECT * FROM users WHERE id = :id && name = :who', [':id' => $id, ':who' => 'Joe']);
```
Return format:
```php
[
[
'id' => 4,
'name' => 'Joe'
],
]
```
<sup>* The class currently replaces `&&` with `AND` if it's surrounded by whitespace on both sides. PostgreSQL would report a syntax error when using the former instead of the latter without this pre-processing step. See the `_alterQuery()` method for the exact implementation.</sup>
If you want the power of a custom query, but also the convenience of getting the first result as a single array you can use `querySingle()` which wortks similarly to `getOne()`.
```php
$db->querySingle("SELECT * FROM 'users' WHERE id = 4");
// Works the same way for bound parameters
$id = 4;
$db->querySingle("SELECT * FROM 'users' WHERE id = ?", [$id]);
```
Return format:
```php
[
'id' => 4,
'name' => 'Joe'
]
```
### Informational methods
**Note:** These will reset the object, so the following will **NOT** work as expected:
```php
$withWhere = $db->where('"id" = 2');
if ($withWhere->has('users')) {
$db->getOne('users');
}
```
Instead of returning the user with the `id` equal to `2`, it'll return whatever row it finds first since the `where()` call is no longer in effect.
#### Check if a table exists [`tableExists():boolean`]
```php
$db->tableExists('users'); // true
```
#### Get number of matching rows [`count():int`]
```php
$db->count('users'); // 3
$db->where('"id" = 2')->count('users'); // 1
```
#### Check if the query returns any rows [`has():boolean`]
Basically `count() >= 1`, but reads nicer.
```php
$db->has('users'); // true
$db->where('id', 2)->has('users'); // true
$db->where('id', 0)->has('users'); // false
```
### Debugging
#### Get last executed query [`getLastQuery():string`]
This will return the last query executed through the class where the placeholders have been replaced with actual values. While the class does its best to keep the SQL valid you should not rely on the returned value being a valid query.
```php
$db->insert('users', ['name' => 'Sarah'])
echo $db->getLastQuery(); // INSERT INTO "users" ("name") VALUES ('Sarah')
```
#### Get last error message [`getLastError():string`]
This can be useful after a failed `insert()`/`update()` call, for example.
```php
// Duplicated ID
$db->insert('users', ['id' => '1', 'name' => 'Fred'])
echo $db->getLastError(); // #1062 - Duplicate entry '1' for key 'PRIMARY'
```
#### Number of rows affected [`count:int`]
Returns the number of rows affected by the last SQL statement
```php
$db->get('users');
echo $db->count; // 3
```
### Extending functionality
### Number of executed queries
With a simple wrapper class it's trivial to include a query counter if need be.
```php
class PostgresDbWrapper extends \SeinopSys\PostgresDb
{
public $query_count = 0;
/**
* @param PDOStatement $stmt Statement to execute
*
* @return bool|array|object[]
*/
protected function execStatement($stmt)
{
$this->query_count++;
return parent::execStatement($stmt);
}
}
```
### Object return values
The class contains two utility methods (`tableNameToClassName` and `setClass`) which allow for the creation of a wrapper that can force returned values into a class instead of an array. This allows for using both the array and class method simultaneously with minimal effort. An example of such a wrapper class is shown below.
This assumes an autoloader is configured within the project which allows classes to be loaded on the fly as needed. This method will cause the autoloader to attempt loading the class within the `Models` namespace when `class_exists` is called. If it fails, a key is set on a private array. This prevents future checks for the existence of the same class to avoid significantly impacting the application's performance.
```php
class PostgresDbWrapper extends \SeinopSys\PostgresDb
{
private $nonexistantClassCache = [];
/**
* @param PDOStatement $stmt Statement to execute
*
* @return bool|array|object[]
*/
protected function execStatement($stmt)
{
$className = $this->tableNameToClassName();
if (isset($className) && empty($this->nonexistantClassCache[$className])) {
try {
if (!class_exists("\\Models\\$className")) {
throw new Exception();
}
$this->setClass("\\Models\\$className");
}
catch (Exception $e) {
$this->nonexistantClassCache[$className] = true;
}
}
return parent::execStatement($stmt);
}
}
```

View File

@@ -0,0 +1,34 @@
{
"name": "seinopsys/postgresql-database-class",
"description": "PHP wrapper class for PDO-based interaction with PostgreSQL databases, heavily based on ThingEngineer's MysqliDb class",
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "SeinopSys",
"email": "seinopsys@gmail.com",
"homepage": "https://github.com/SeinopSys",
"role": "Developer"
}
],
"require": {
"php": ">=5.4",
"ext-pdo": "*",
"ext-pdo_pgsql": "*"
},
"require-dev": {
"squizlabs/php_codesniffer": "3.*"
},
"autoload": {
"psr-4": {
"SeinopSys\\": "src/"
}
},
"scripts": {
"test": "@php test.php",
"lint": "vendor/bin/phpcs . --standard=ruleset.xml"
},
"scripts-descriptions": {
"test": "Run tests - likely going to fail unless running on Travis CI",
"lint": "Run PHP_CodeSniffer with the project's ruleset"
}
}

View File

@@ -0,0 +1,73 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "994a8614562650fa5ad11e62baec0052",
"packages": [],
"packages-dev": [
{
"name": "squizlabs/php_codesniffer",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e",
"reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
"bin/phpcs",
"bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "http://www.squizlabs.com/php-codesniffer",
"keywords": [
"phpcs",
"standards"
],
"time": "2018-09-23T23:08:17+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.4",
"ext-pdo": "*",
"ext-pdo_pgsql": "*"
},
"platform-dev": []
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<ruleset name="PostgresDb">
<description>PSR2 with a few tweaks</description>
<exclude-pattern>vendor/.*</exclude-pattern>
<exclude-pattern>test.php</exclude-pattern>
<arg name="report" value="code"/>
<rule ref="PSR2"/>
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Squiz.Classes.ValidClassName"/>
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
<rule ref="Squiz.NamingConventions.ValidVariableName"/>
<rule ref="Squiz.NamingConventions.ValidVariableName.PrivateNoUnderscore">
<severity>0</severity>
</rule>
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace">
<type>error</type>
</rule>
<rule ref="Squiz.Strings.DoubleQuoteUsage">
<type>error</type>
</rule>
<rule ref="Squiz.Strings.DoubleQuoteUsage.ContainsVar">
<severity>0</severity>
</rule>
</ruleset>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,459 @@
<?php
$_ = [
'TESTDB_CONNECTION_ERROR' => 0xFFF,
'TABLEEXISTS_NOT_FALSE' => 0x100,
'TABLEEXISTS_NOT_TRUE' => 0x101,
'GET_QUERY_MISMATCH' => 0x200,
'GET_RETURNING_WRONG_DATA' => 0x201,
'GET_QUERY_LIMIT_MISMATCH' => 0x202,
'GET_QUERY_ARRAY_LIMIT_MISMATCH' => 0x203,
'GET_RETURN_MISSING_RESULT' => 0x204,
'GET_RETURN_TOO_MANY_RESULT' => 0x205,
'GET_RETURN_WRONG_RESULT_TYPE_STRING' => 0x206,
'GET_RETURN_WRONG_RESULT_TYPE_INT' => 0x207,
'INSERT_QUERY_MISMATCH' => 0x300,
'INSERT_RETURN_WRONG_DATA' => 0x301,
'INSERT_RETURN_WRONG_DATA_TYPE_INT' => 0x302,
'INSERT_RETURN_WRONG_DATA_TYPE_STRING' => 0x303,
'INSERT_DUPE_PRIMARY_KEY_WRONG_ERROR_MSG' => 0x304,
'INSERT_RETURN_WRONG_DATA_TYPE_ARRAY' => 0x305,
'GETONE_QUERY_MISMATCH' => 0x400,
'GETONE_RETURNING_WRONG_STRUCTURE' => 0x401,
'GETONE_RETURNING_WRONG_DATA' => 0x402,
'GETONE_QUERY_COLUMN_MISMATCH' => 0x403,
'GETONE_RETURNING_WRONG_DATA_TYPE_INT' => 0x402,
'GETONE_RETURNING_WRONG_DATA_TYPE_STRING' => 0x402,
'GETONE_RETURNING_COLUMN_WRONG_DATA' => 0x403,
'WHERE_QUERY_MISMATCH' => 0x500,
'WHERE_RETURNING_WRONG_DATA' => 0x501,
'WHERE_QUERY_LITERAL_MISMATCH' => 0x502,
'WHERE_QUERY_ARRAY_MISMATCH' => 0x503,
'WHERE_RETURNING_WRONG_DATA_TYPE_INT' => 0x504,
'WHERE_RETURNING_WRONG_DATA_TYPE_STRING' => 0x505,
'WHERE_QUERY_BETWEEN_ARRAY_MISMATCH' => 0x506,
'WHERE_QUERY_NULL_MISMATCH' => 0x507,
'ORDERBY_QUERY_MISMATCH' => 0x600,
'ORDERBY_RETURNING_WRONG_DATA' => 0x601,
'ORDERBY_RETURNING_WRONG_DATA_TYPE_INT' => 0x602,
'ORDERBY_RETURNING_WRONG_DATA_TYPE_STRING' => 0x603,
'QUERY_QUERY_MISMATCH' => 0x700,
'QUERY_RETURNING_WRONG_DATA' => 0x701,
'QUERY_ARRAY_QUERY_MISMATCH' => 0x702,
'QUERY_ARRAY_RETURNING_WRONG_DATA' => 0x703,
'COUNT_QUERY_MISMATCH' => 0x800,
'COUNT_RETURNING_WRONG_DATA_TYPE' => 0x801,
'COUNT_RETURNING_WRONG_DATA' => 0x802,
'HAS_RETURNING_WRONG_DATA_TYPE' => 0x900,
'HAS_RETURNING_WRONG_DATA' => 0x901,
'DELETE_WHERE_NOT_DELETING' => 0xA00,
'DELETE_WHERE_DELETING_WRONG_ROWS' => 0xA01,
'DELETE_NOT_DELETING' => 0xA02,
'DELETE_QUERY_MISMATCH' => 0xA03,
'DELETE_RETURNING_WRONG_DATA' => 0xA04,
'DELETE_RETURNING_WRONG_DATA_TYPE_STRING' => 0xA05,
'JOIN_QUERY_MISMATCH' => 0xB00,
'JOIN_QUERY_ALIAS_MISMATCH' => 0xB01,
'PDO_ERRMODE_UNCHANGED_PRECONN' => 0xC00,
'PDO_ERRMODE_UNCHANGED_POSTCONN' => 0xC01,
'GROUPBY_QUERY_MISMATCH' => 0xD00,
'GROUPBY_RETURNING_WRONG_DATA' => 0xD01,
'STRING_MISMATCH' => 0xFFF,
];
function fail($exit_key)
{
global $_;
if (!isset($_[$exit_key])) {
throw new RuntimeException("FAILURE: $exit_key (invalid exit code)");
}
$RawExitCode = $_[$exit_key];
$ExitCode = '0x' . strtoupper(dechex($RawExitCode));
throw new RuntimeException("FAILURE: $exit_key ($ExitCode)\n");
}
function expect($generated, $expect, $exit_key = 'STRING_MISMATCH')
{
if ($expect !== $generated) {
echo "Mismatched string\nExpected: ", var_export($expect, true), "\n Got: ", var_export($generated, true), "\n";
fail($exit_key);
}
}
require __DIR__ . '/src/PostgresDb.php';
use \SeinopSys\PostgresDb;
# replacePlaceHolders() checks
expect(
PostgresDb::replacePlaceHolders('SELECT * FROM users WHERE id = ? AND name = ?', [1, 'John']),
"SELECT * FROM users WHERE id = 1 AND name = 'John'"
);
expect(
PostgresDb::replacePlaceHolders('SELECT * FROM users WHERE id = :id AND name = :un', [':id' => 1, ':un' => 'John']),
"SELECT * FROM users WHERE id = 1 AND name = 'John'"
);
expect(
PostgresDb::replacePlaceHolders('SELECT * FROM users WHERE id = :id AND name = ?', [':id' => 1, 'John']),
"SELECT * FROM users WHERE id = 1 AND name = 'John'"
);
expect(
PostgresDb::replacePlaceHolders('SELECT * FROM users WHERE id = :id AND name = ? AND role = ? AND password IS :pw', [':id' => 1, 'John', ':pw' => null, 'admin']),
"SELECT * FROM users WHERE id = 1 AND name = 'John' AND role = 'admin' AND password IS NULL"
);
expect(
PostgresDb::replacePlaceHolders('SELECT * FROM users WHERE id = :id AND name = ? AND role = ? AND password IS :pw', ['id' => 1, 'John', 'pw' => null, 'admin']),
"SELECT * FROM users WHERE id = 1 AND name = 'John' AND role = 'admin' AND password IS NULL"
);
$db = new PostgresDb('test', 'localhost', 'postgres', '');
function checkQuery($expect, $exit_key)
{
global $db;
if ($db === null) {
return false;
}
expect($db->getLastQuery(), $expect, $exit_key);
}
// Check tableExists & query
try {
$db->getConnection();
} catch (Throwable $e) {
var_dump($e);
fail('TESTDB_CONNECTION_ERROR');
}
if ($db->tableExists('users') !== false) {
fail('TABLEEXISTS_NOT_FALSE');
}
$db->query('CREATE TABLE "users" (id SERIAL NOT NULL, name CHARACTER VARYING(10), gender CHARACTER(1) NOT NULL)');
if ($db->tableExists('users') !== true) {
fail('TABLEEXISTS_NOT_TRUE');
}
// Add PRIMARY KEY constraint
$db->query('ALTER TABLE "users" ADD CONSTRAINT "users_id" PRIMARY KEY ("id")');
# get() Checks
// Regular call
$users = $db->get('users');
checkQuery('SELECT * FROM "users"', 'GET_QUERY_MISMATCH');
if (!is_array($users)) {
var_dump($users);
fail('GET_RETURNING_WRONG_DATA');
}
// Check get with limit
$users = $db->get('users', 1);
checkQuery('SELECT * FROM "users" LIMIT 1', 'GET_QUERY_LIMIT_MISMATCH');
// Check get with array limit
$users = $db->get('users', [10, 2]);
checkQuery('SELECT * FROM "users" LIMIT 2 OFFSET 10', 'GET_QUERY_ARRAY_LIMIT_MISMATCH');
// Check get with column(s)
$users = $db->get('users', null, 'id');
checkQuery('SELECT id FROM "users"', 'GET_QUERY_COLUMNS_MISMATCH');
// Check get with complex column(s)
$users = $db->get('users', null, "'ayy-'||id as happy_id");
checkQuery("SELECT 'ayy-'||id AS happy_id FROM \"users\"", 'GET_QUERY_COLUMNS_MISMATCH');
// Check get with column aliases
$users = $db->get('users', null, 'id as user_id');
checkQuery('SELECT id AS user_id FROM "users"', 'GET_QUERY_COLUMNS_MISMATCH');
$users = $db->get('users', null, 'id as limit, name as from');
checkQuery('SELECT id AS "limit", "name" AS "from" FROM "users"', 'GET_QUERY_COLUMNS_MISMATCH');
# Check PDO error mode setting
$db->setPDOErrmode(PDO::ERRMODE_EXCEPTION);
$caught = false;
try {
// There's no email column so we should get an exception
$db->getOne('users', 'email');
} catch (PDOException $e) {
$caught = true;
}
if (!$caught) {
fail('PDO_ERRMODE_UNCHANGED_POSTCONN');
}
# count() Checks
// Call
$count = $db->count('users');
checkQuery('SELECT COUNT(*) AS cnt FROM "users" LIMIT 1', 'COUNT_QUERY_MISMATCH');
if (!is_int($count)) {
fail('COUNT_RETURNING_WRONG_DATA_TYPE');
}
if ($count !== 0) {
fail('COUNT_RETURNING_WRONG_DATA');
}
# has() Checks
// Call
$has = $db->has('users');
if (!is_bool($has)) {
fail('HAS_RETURNING_WRONG_DATA_TYPE');
}
if ($has !== false) {
fail('HAS_RETURNING_WRONG_DATA');
}
# insert() Checks
$return = $db->insert('users', ['name' => 'David', 'gender' => 'm']);
checkQuery('INSERT INTO "users" ("name", gender) VALUES (\'David\', \'m\')', 'INSERT_QUERY_MISMATCH');
if ($return !== true) {
fail('INSERT_RETURN_WRONG_DATA');
}
# get() format checks
$users = $db->get('users');
if (!is_array($users)) {
fail('GET_RETURNING_WRONG_DATA');
}
if (!isset($users[0])) {
fail('GET_RETURN_MISSING_RESULT');
}
if (isset($users[1])) {
fail('GET_RETURN_TOO_MANY_RESULT');
}
if (!is_string($users[0]['name'])) {
fail('GET_RETURN_WRONG_RESULT_TYPE_STRING');
}
if (!is_int($users[0]['id'])) {
fail('GET_RETURN_WRONG_RESULT_TYPE_INT');
}
// Check insert with returning integer
$id = $db->insert('users', ['name' => 'Jon', 'gender' => 'm'], 'id');
checkQuery('INSERT INTO "users" ("name", gender) VALUES (\'Jon\', \'m\') RETURNING id', 'INSERT_QUERY_MISMATCH');
if (!is_int($id)) {
fail('INSERT_RETURN_WRONG_DATA_TYPE_INT');
}
if ($id !== 2) {
fail('INSERT_RETURN_WRONG_DATA');
}
// Check insert with returning string
$name = $db->insert('users', ['name' => 'Anna', 'gender' => 'f'], 'name');
checkQuery('INSERT INTO "users" ("name", gender) VALUES (\'Anna\', \'f\') RETURNING "name"', 'INSERT_QUERY_MISMATCH');
if (!is_string($name)) {
fail('INSERT_RETURN_WRONG_DATA_TYPE_STRING');
}
if ($name !== 'Anna') {
fail('INSERT_RETURN_WRONG_DATA');
}
// Check insert with returning multiple columns
$return = $db->insert('users', ['name' => 'Jason', 'gender' => 'm'], 'name, gender');
checkQuery('INSERT INTO "users" ("name", gender) VALUES (\'Jason\', \'m\') RETURNING "name", gender', 'INSERT_QUERY_MISMATCH');
if (!is_array($return)) {
fail('INSERT_RETURN_WRONG_DATA_TYPE_ARRAY');
}
if ($return['name'] !== 'Jason' || $return['gender'] !== 'm') {
fail('INSERT_RETURN_WRONG_DATA');
}
# where() Checks
// Generic use
$id1 = $db->where('id', 1)->get('users');
checkQuery('SELECT * FROM "users" WHERE id = 1', 'WHERE_QUERY_MISMATCH');
if (empty($id1) || !isset($id1[0]['id'])) {
fail('WHERE_RETURNING_WRONG_DATA');
}
if ($id1[0]['id'] !== 1) {
fail('WHERE_RETURNING_WRONG_DATA_TYPE_INT');
}
if ($id1[0]['name'] !== 'David') {
fail('WHERE_RETURNING_WRONG_DATA_TYPE_STRING');
}
// Literal string check
$id1 = $db->where('"id" = 1')->get('users');
checkQuery('SELECT * FROM "users" WHERE "id" = 1', 'WHERE_QUERY_LITERAL_MISMATCH');
if (empty($id1) || !isset($id1[0]['id']) || $id1[0]['id'] != 1) {
fail('WHERE_RETURNING_WRONG_DATA');
}
if (!is_int($id1[0]['id'])) {
fail('WHERE_RETURNING_WRONG_DATA_TYPE_INT');
}
// Null equality check
$db->where('id', null)->get('users');
checkQuery('SELECT * FROM "users" WHERE id IS NULL', 'WHERE_QUERY_NULL_MISMATCH');
// Array check
$id1 = $db->where('id', [1, 2])->orderBy('id')->get('users');
checkQuery('SELECT * FROM "users" WHERE id IN (1, 2) ORDER BY id ASC', 'WHERE_QUERY_ARRAY_MISMATCH');
if (empty($id1) || !isset($id1[0]['id'], $id1[1]['id']) || $id1[0]['id'] != 1 || $id1[1]['id'] != 2) {
fail('WHERE_RETURNING_WRONG_DATA');
}
if (!is_int($id1[0]['id']) || !is_int($id1[1]['id'])) {
fail('WHERE_RETURNING_WRONG_DATA_TYPE_INT');
}
$db->where('id', [1, 2], '!=')->orderBy('id')->get('users');
checkQuery('SELECT * FROM "users" WHERE id NOT IN (1, 2) ORDER BY id ASC', 'WHERE_QUERY_ARRAY_MISMATCH');
// Between array check
$id1 = $db->where('id', [1, 2], 'BETWEEN')->orderBy('id')->get('users');
checkQuery('SELECT * FROM "users" WHERE id BETWEEN 1 AND 2 ORDER BY id ASC', 'WHERE_QUERY_BETWEEN_ARRAY_MISMATCH');
if (empty($id1) || !isset($id1[0]['id'], $id1[1]['id']) || $id1[0]['id'] != 1 || $id1[1]['id'] != 2) {
fail('WHERE_RETURNING_WRONG_DATA');
}
if (!is_int($id1[0]['id']) || !is_int($id1[1]['id'])) {
fail('WHERE_RETURNING_WRONG_DATA_TYPE_INT');
}
# getOne() Checks
// Generic call
$first_user = $db->where('id', 1)->getOne('users');
checkQuery('SELECT * FROM "users" WHERE id = 1 LIMIT 1', 'GETONE_QUERY_MISMATCH');
if (isset($first_user[0]) && is_array($first_user[0])) {
fail('GETONE_RETURNING_WRONG_STRUCTURE');
}
if ($first_user['id'] != 1) {
fail('GETONE_RETURNING_COLUMN_WRONG_DATA');
}
if (!is_int($first_user['id'])) {
fail('GETONE_RETURNING_WRONG_DATA_TYPE_INT');
}
if (!is_string($first_user['name'])) {
fail('GETONE_RETURNING_WRONG_DATA_TYPE_STRING');
}
// Columns
$first_user = $db->where('id', 1)->getOne('users', 'id, name');
checkQuery('SELECT id, "name" FROM "users" WHERE id = 1 LIMIT 1', 'GETONE_QUERY_COLUMN_MISMATCH');
if ($first_user['id'] != 1) {
fail('GETONE_RETURNING_COLUMN_WRONG_DATA');
}
if (!is_int($first_user['id'])) {
fail('GETONE_RETURNING_WRONG_DATA_TYPE_INT');
}
if (!is_string($first_user['name'])) {
fail('GETONE_RETURNING_WRONG_DATA_TYPE_STRING');
}
# orderBy() Checks
// Generic call
$last_user = $db->orderBy('id', 'DESC')->getOne('users');
checkQuery('SELECT * FROM "users" ORDER BY id DESC LIMIT 1', 'ORDERBY_QUERY_MISMATCH');
if (!isset($last_user['id'])) {
fail('ORDERBY_RETURNING_WRONG_DATA');
}
if ($last_user['id'] != 4) {
fail('ORDERBY_RETURNING_WRONG_DATA');
}
if (!is_int($last_user['id'])) {
fail('ORDERBY_RETURNING_WRONG_DATA_TYPE_INT');
}
if (!is_string($last_user['name'])) {
fail('ORDERBY_RETURNING_WRONG_DATA_TYPE_STRING');
}
# groupBy() Checks
// Generic call
$gender_count = $db->groupBy('gender')->orderBy('cnt', 'DESC')->get('users', null, 'gender, COUNT(*) as cnt');
checkQuery('SELECT gender, COUNT(*) AS cnt FROM "users" GROUP BY gender ORDER BY cnt DESC', 'GROUPBY_QUERY_MISMATCH');
if (!isset($gender_count[0]['cnt'], $gender_count[1]['cnt'], $gender_count[0]['gender'], $gender_count[1]['gender'])) {
fail('GROUPBY_RETURNING_WRONG_DATA');
}
if ($gender_count[0]['cnt'] !== 3 || $gender_count[1]['cnt'] !== 1 || $gender_count[0]['gender'] !== 'm' || $gender_count[1]['gender'] !== 'f') {
fail('GROUPBY_RETURNING_WRONG_DATA');
}
# query() Checks
// No bound parameters
$first_two_users = $db->query('SELECT * FROM "users" WHERE id <= 2');
checkQuery('SELECT * FROM "users" WHERE id <= 2', 'QUERY_QUERY_MISMATCH');
if (!is_array($first_two_users)) {
fail('QUERY_RETURNING_WRONG_DATA');
}
if (count($first_two_users) !== 2) {
fail('QUERY_RETURNING_WRONG_DATA');
}
// Bound parameters
$first_two_users = $db->query('SELECT * FROM "users" WHERE id <= ?', [2]);
checkQuery('SELECT * FROM "users" WHERE id <= 2', 'QUERY_QUERY_MISMATCH');
if (!is_array($first_two_users)) {
fail('QUERY_RETURNING_WRONG_DATA');
}
if (count($first_two_users) !== 2) {
fail('QUERY_RETURNING_WRONG_DATA');
}
# getLastError Check
$caught = false;
try {
// An entry with an id of 1 already exists, we should get an exception
$Insert = @$db->insert('users', ['id' => 1, 'name' => 'Sam', 'gender' => 'm']);
} catch (PDOException $e) {
$caught = true;
}
if (!$caught) {
fail('INSERT_DUPE_PRIMARY_KEY_NOT_RECOGNIZED');
}
if (strpos($db->getLastError(), 'duplicate key value violates unique constraint') === false) {
fail('INSERT_DUPE_PRIMARY_KEY_WRONG_ERROR_MSG');
}
# count() Re-check
// Call
$count = $db->count('users');
if ($count !== 4) {
fail('COUNT_RETURNING_WRONG_DATA');
}
# has() Checks
// Call
$has = $db->has('users');
if (!is_bool($has)) {
fail('HAS_RETURNING_WRONG_DATA_TYPE');
}
if ($has !== true) {
fail('HAS_RETURNING_WRONG_DATA');
}
# delete() Checks
// Call with where()
$db->where('id', 3)->delete('users');
checkQuery('DELETE FROM "users" WHERE id = 3', 'DELETE_QUERY_MISMATCH');
if ($db->where('id', 3)->has('users')) {
fail('DELETE_WHERE_NOT_DELETING');
}
if ($db->where('id', 3, '<')->count('users') !== 2) {
fail('DELETE_WHERE_DELETING_WRONG_ROWS');
}
// Standalone call
$db->delete('users');
checkQuery('DELETE FROM "users"', 'DELETE_QUERY_MISMATCH');
if ($db->has('users')) {
fail('DELETE_NOT_DELETING');
}
// Array
$db->where('id', [3, 4])->delete('users');
checkQuery('DELETE FROM "users" WHERE id IN (3, 4)', 'DELETE_QUERY_MISMATCH');
// Returning data
$db->insert('users', ['id' => 10, 'name' => 'Ada', 'gender' => 'f']);
$return = $db->where('id', 10)->delete('users', ['name','gender']);
checkQuery('DELETE FROM "users" WHERE id = 10 RETURNING "name", gender', 'DELETE_QUERY_MISMATCH');
if (!is_array($return)) {
fail('DELETE_RETURNING_WRONG_DATA');
}
if (!isset($return['name'], $return['gender']) || $return['name'] !== 'Ada' || $return['gender'] !== 'f') {
fail('DELETE_RETURNING_WRONG_DATA_TYPE_STRING');
}
# join() Checks
$db->query('CREATE TABLE "userdata" (id SERIAL NOT NULL, somevalue INTEGER)');
$db->insert('userdata', ['somevalue' => 1]);
$db->join('userdata', 'userdata.id = users.id', 'LEFT')->where('users.id', 1)->get('users', null, 'users.*');
checkQuery('SELECT "users".* FROM "users" LEFT JOIN "userdata" ON userdata.id = users.id WHERE "users".id = 1', 'JOIN_QUERY_MISMATCH');
$db->join('userdata ud', 'ud.id = u.id', 'LEFT')->where('u.id', 1)->get('users u', null, 'u.*');
checkQuery('SELECT "u".* FROM "users" u LEFT JOIN "userdata" ud ON ud.id = u.id WHERE "u".id = 1', 'JOIN_QUERY_ALIAS_MISMATCH');