<?php // phpcs:disable Generic.Files.LineLength

/*
*** IMPORTANT ***
This test needs a working database with certain tables setup
DB Host: localhost
DB Name: corelibs_db_io_test
DB User: corelibs_db_io_test
DB Password: corelibs_db_io_test
DB Encoding: UTF8 (MUST!)
User must be able to drop/create tables
In case of changes the valid_* $db_config entries must be changed
*** IMPORTANT ***

Below tables will be automatically created
Table with Primary Key: table_with_primary_key
Table without Primary Key: table_without_primary_key

Table with primary key has additional row:
row_primary_key	INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
Each table has the following rows
row_int INT,
row_numeric NUMERIC,
row_varchar VARCHAR,
row_json JSON
row_jsonb JSONB,
row_bytea BYTEA,
row_timestamp TIMESTAMP WITHOUT TIME ZONE,
row_date DATE,
row_interval INTERVAL,

*/

declare(strict_types=1);

namespace tests;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use CoreLibs\Logging;
use CoreLibs\DB\Options\Convert;
use CoreLibs\DB\Support\ConvertPlaceholder;

/**
 * Test class for DB\IO + DB\SQL\PgSQL
 * This will only test the PgSQL parts
 * @coversDefaultClass \CoreLibs\DB\IO
 * @coversDefaultClass \CoreLibs\DB\SQL\PgSQL
 * @testdox \CoreLibs\DB\IO method tests for SQL\PgSQL
 */
final class CoreLibsDBIOTest extends TestCase
{
	private static $db_config = [
		// self localhost/ip connection
		'valid' => [
			'db_name' => 'corelibs_db_io_test',
			'db_user' => 'corelibs_db_io_test',
			'db_pass' => 'corelibs_db_io_test',
			'db_host' => 'localhost',
			'db_port' => 5432,
			'db_schema' => 'public',
			'db_type' => 'pgsql',
			'db_encoding' => '',
			'db_ssl' => 'allow', // allow, disable, require, prefer
		],
		// same as valid, but encoding is set
		'valid_with_encoding_utf8' => [
			'db_name' => 'corelibs_db_io_test',
			'db_user' => 'corelibs_db_io_test',
			'db_pass' => 'corelibs_db_io_test',
			'db_host' => 'localhost',
			'db_port' => 5432,
			'db_schema' => 'public',
			'db_type' => 'pgsql',
			'db_encoding' => 'UTF-8',
			'db_ssl' => 'allow', // allow, disable, require, prefer
		],
		// valid with no schema set
		'valid_no_schema' => [
			'db_name' => 'corelibs_db_io_test',
			'db_user' => 'corelibs_db_io_test',
			'db_pass' => 'corelibs_db_io_test',
			'db_host' => 'localhost',
			'db_port' => 5432,
			'db_schema' => '',
			'db_type' => 'pgsql',
			'db_encoding' => '',
			'db_ssl' => 'allow', // allow, disable, require, prefer
		],
		// invalid (missing db name)
		'invalid' => [
			'db_name' => '',
			'db_user' => '',
			'db_pass' => '',
			'db_host' => '',
			'db_port' => 5432,
			'db_schema' => 'public',
			'db_type' => 'pgsql',
			'db_encoding' => '',
			'db_ssl' => 'allow', // allow, disable, require, prefer
		],
	];
	private static $log;

	/**
	 * Test if pgsql module loaded
	 * Check if valid DB connection works
	 * Check if tables exist and remove them
	 * Create test tables
	 *
	 * @return void
	 */
	public static function setUpBeforeClass(): void
	{
		if (!extension_loaded('pgsql')) {
			self::markTestSkipped(
				'The PgSQL extension is not available.'
			);
		}
		// define basic connection set valid and one invalid
		self::$log = new Logging\Logging([
			// 'log_folder' => __DIR__ . DIRECTORY_SEPARATOR . 'log',
			'log_folder' => DIRECTORY_SEPARATOR . 'tmp',
			'log_file_id' => 'CoreLibs-DB-IO-Test',
		]);
		// will be true, default logging is true
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		if (!$db->dbGetConnectionStatus()) {
			self::markTestSkipped(
				'Cannot connect to valid Test DB for DB\IO test.'
			);
		}
		// check if they already exist, drop them
		if ($db->dbShowTableMetaData('table_with_primary_key') !== false) {
			$db->dbExec("CREATE EXTENSION IF NOT EXISTS pgcrypto");
			$db->dbExec("DROP TABLE table_with_primary_key");
			$db->dbExec("DROP TABLE table_without_primary_key");
			$db->dbExec("DROP TABLE test_meta");
		}
		// uid is for internal reference tests
		$base_table = <<<SQL
			uid VARCHAR,
			row_int INT,
			row_numeric NUMERIC,
			row_varchar VARCHAR,
			row_varchar_literal VARCHAR,
			row_json JSON,
			row_jsonb JSONB,
			row_bytea BYTEA,
			row_timestamp TIMESTAMP WITHOUT TIME ZONE,
			row_date DATE,
			row_interval INTERVAL,
			row_array_int INT ARRAY,
			row_array_varchar VARCHAR ARRAY
		)
		WITHOUT OIDS
		SQL;
		// create the tables
		$db->dbExec(
			// primary key name is table + '_id'
			<<<SQL
			CREATE TABLE table_with_primary_key (
				table_with_primary_key_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
				$base_table
			SQL
		);
		$db->dbExec(
			<<<SQL
			CREATE TABLE table_without_primary_key (
				$base_table
			SQL
			/* "CREATE TABLE table_without_primary_key ("
			. $base_table */
		);
		// create simple table for meta test
		$db->dbExec(
			<<<SQL
			CREATE TABLE test_meta (
				row_1 VARCHAR,
				row_2 INT
			) WITHOUT OIDS
			SQL
			/* "CREATE TABLE test_meta ("
			. "row_1 VARCHAR, "
			. "row_2 INT"
			. ") WITHOUT OIDS" */
		);
		// set some test schema
		$db->dbExec("CREATE SCHEMA IF NOT EXISTS testschema");
		// end connection
		$db->dbClose();
	}

	/**
	 * Check that we can actually do these tests
	 *
	 * @return void
	 */
	protected function setUp(): void
	{
		// print_r(self::$db_config);
	}

	/**
	 * For all Warning and Error checks in all tests
	 * DB Warning/Error checks
	 *
	 * @param  \CoreLibs\DB\IO $db
	 * @param  string          $warning
	 * @param  string          $error
	 * @return array
	 */
	private function subAssertErrorTest(
		\CoreLibs\DB\IO $db,
		string $warning,
		string $error
	): array {
		// get last error/warnings
		$last_warning = $db->dbGetLastWarning();
		$last_error = $db->dbGetLastError();
		// if string for warning or error is not empty check
		$this->assertEquals(
			$warning,
			$last_warning,
			'Assert query warning'
		);
		$this->assertEquals(
			$error,
			$last_error,
			'Assert query error'
		);
		return [$last_warning, $last_error];
	}

	// - connected version test
	//   dbVerions, dbVersionNum, dbVersionInfo, dbVersionInfoParameters,
	//   dbCompareVersion

	/**
	 * Just checks that the return value of dbVersion matches basic regex
	 *
	 * @covers ::dbVersion
	 * @testdox test db version string return matching retex
	 *
	 * @return void
	 */
	public function testDbVersion(): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		$this->assertMatchesRegularExpression(
			"/^\d+\.\d+(\.\d+)?$/",
			$db->dbVersion()
		);

		$db->dbClose();
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbVersionNumeric
	 * @testdox test db version numeric return matching retex
	 *
	 * @return void
	 */
	public function testDbVersionNumeric(): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		$version = $db->dbVersionNumeric();
		// must be int
		$this->assertIsInt($version);
		// assume 90606 or 130006
		// should this change, the regex below must change
		$this->assertMatchesRegularExpression(
			"/^\d{5,6}?$/",
			(string)$version
		);

		$db->dbClose();
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbVersionInfoParameters
	 * @testdox test db version parameters are returned as array
	 *
	 * @return void
	 */
	public function testDbVersionInfoParameters(): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		$parameters = $db->dbVersionInfoParameters();

		$this->assertIsArray($parameters);
		$this->assertGreaterThan(
			1,
			count($parameters)
		);
		// must have at least this
		$this->assertContains(
			'server',
			$parameters
		);

		$db->dbClose();
	}

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function versionInfoProvider(): array
	{
		return [
			'client' => [
				'client',
				"/^\d+\.\d+/"
			],
			'session authorization' => [
				'session_authorization',
				"/^\w+$/"
			],
			'test non existing' => [
				'non_existing',
				'/^$/'
			],
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbVersionInfo
	 * @dataProvider versionInfoProvider
	 * @testdox Version Info $parameter matches as $expected [$_dataName]
	 *
	 * @param string $parameter
	 * @param string $expected
	 * @return void
	 */
	public function testDbVersionInfo(string $parameter, string $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		$this->assertMatchesRegularExpression(
			$expected,
			$db->dbVersionInfo($parameter)
		);

		$db->dbClose();
	}

	/**
	 * Returns test list for dbCompareVersion check
	 * NOTE: unless we fully mock the =version check needs to be updated
	 *
	 * @return array
	 */
	public function versionProvider(): array
	{
		// 1: vesion to compare to
		// 2: expected outcome
		return [
			'compare = ok' => [ '=13.6.0', true ],
			'compare = bad' => [ '=9.2.0', false ],
			'compare < ok' => [ '<99.0.0', true ],
			'compare < bad' => [ '<9.2.0', false ],
			'compare <= ok a' => [ '<=99.0.0', true ],
			'compare <= ok b' => [ '<=13.6.0', true ],
			'compare <= false' => [ '<=9.2.0', false ],
			'compare > ok' => [ '>9.2.0', true ],
			'compare > bad' => [ '>99.2.0', false ],
			'compare >= ok a' => [ '>=13.6.0', true ],
			'compare >= ok b' => [ '>=9.2.0', true ],
			'compare >= bad' => [ '>=99.0.0', false ],
		];
	}

	/**
	 * NOTE
	 * Version tests will fail if versions change
	 * Current base as Version 13.6 for equal check
	 * I can't mock a function on the same class when it is called in a method
	 * NOTE
	 *
	 * @covers ::dbCompareVersion
	 * @dataProvider versionProvider
	 * @testdox Version $input compares as $expected [$_dataName]
	 *
	 * @return void
	 */
	public function testDbVerson(string $input, bool $expected): void
	{
		/** @var \CoreLibs\DB\IO&MockObject $db_io_mock */
		$db_io_mock = $this->createPartialMock(\CoreLibs\DB\IO::class, ['dbVersion']);
		$db_io_mock->method('dbVersion')->willReturn('13.6.0');

		$this->assertEquals(
			$expected,
			// $db->dbCompareVersion($input)
			$db_io_mock->dbCompareVersion($input)
		);
	}

	// - connect to DB test (dbGetConnectionStatus)
	// - connected get dbInfo data check (show true, false)
	// - disconnect: dbClose

	/**
	 * connection DB strings list with info blocks for connection testing
	 *
	 * @return array
	 */
	public function connectionProvider(): array
	{
		// 0: connection array
		// 1: status after connection
		// 2: exception name
		// 2: info string
		// 3: ???
		return [
			'invalid connection' => [
				self::$db_config['invalid'],
				false,
				'RuntimeException',
				"-DB-info-> Connected to db '' with schema 'public' as user "
					. "'' at host '' on port '5432' with ssl mode 'allow' **** "
					. "-DB-info-> DB IO Class debug output: Yes **** ",
				null,
			],
			'valid connection' => [
				self::$db_config['valid'],
				true,
				'',
				"-DB-info-> Connected to db 'corelibs_db_io_test' with "
					. "schema 'public' as user 'corelibs_db_io_test' at host "
					. "'localhost' on port '5432' with ssl mode 'allow' **** "
					. "-DB-info-> DB IO Class debug output: Yes **** ",
				null,
			],
		];
	}

	/**
	 * Connection tests and confirmation with info blocks
	 *
	 * @covers ::__connectToDB
	 * @dataProvider connectionProvider
	 * @testdox Connection will be $expected [$_dataName]
	 *
	 * @param  array  $connection
	 * @param  bool   $expected_status
	 * @param  string   $exception
	 * @param  string $expected_string
	 * @return void
	 */
	public function testConnection(
		array $connection,
		bool $expected_status,
		string $exception,
		string $expected_string
	): void {
		if ($expected_status === false) {
			$this->expectException($exception);
		}
		$db = new \CoreLibs\DB\IO(
			$connection,
			self::$log
		);
		$this->assertEquals(
			$expected_status,
			$db->dbGetConnectionStatus(),
		);
		$this->assertEquals(
			$expected_string,
			$db->dbInfo(false, true)
		);

		// print "DB: " . $db->dbInfo(false, true) . "\n";
		if ($db->dbGetConnectionStatus()) {
			// db close check
			$db->dbClose();
			$this->assertEquals(
				false,
				$db->dbGetConnectionStatus()
			);
		} else {
			// TODO: error checks
			// print "LAST ERROR: " . $db->dbGetLastError(true) . "\n";
			// print "ERRORS: " . print_r($db->dbGetCombinedErrorHistory(), true) . "\n";
		}
	}

	// - debug flag sets
	//   dbGetDebug,  dbSetDebug, dbToggleDebug

	/**
	 * test set for setDebug
	 *
	 * @return array
	 */
	public function debugSetProvider(): array
	{
		// 0: db connecdtion
		// 1: override log flag, null for default
		// 2: set flag
		return [
			'default debug set' => [
				// what base connection
				'valid',
				// actions (set)
				null,
				// set exepected
				true,
			],
			'set debug to false' => [
				'valid',
				false,
				false,
			]
		];
	}

	/**
	 * Test dbSetDbug, dbGetDebug
	 *
	 * @covers ::dbGetDbug
	 * @covers ::dbSetDebug
	 * @testdox Set and Get Debug flag
	 *
	 * @return void
	 */
	public function testDbSetDebug(): void
	{
		$connection = 'valid';
		// default set, expect true
		$db = new \CoreLibs\DB\IO(
			self::$db_config[$connection],
			self::$log
		);
		$this->assertTrue(
			$db->dbGetDebug()
		);
		// switch off
		$db->dbSetDebug(false);
		$this->assertFalse(
			$db->dbGetDebug()
		);
		$db->dbClose();
		// second conenction with log set NOT debug
		$log = new Logging\Logging([
			// 'log_folder' => __DIR__ . DIRECTORY_SEPARATOR . 'log',
			'log_folder' => DIRECTORY_SEPARATOR . 'tmp',
			'log_file_id' => 'CoreLibs-DB-IO-Test',
			'log_level' => Logging\Logger\Level::Notice,
		]);
		$db = new \CoreLibs\DB\IO(
			self::$db_config[$connection],
			$log
		);
		$this->assertFalse(
			$db->dbGetDebug()
		);
	}

	// - set max query call sets
	//   dbSetMaxQueryCall, dbGetMaxQueryCall

	/**
	 * test list for max query run settings
	 *
	 * @return array
	 */
	public function maxQueryCallProvider(): array
	{
		// 0: max call number
		// 1: expected flag from set call
		// 2: expected number from get call
		// 3: expected last warning id
		// 4: expected last error id
		return [
			'set default' => [
				null,
				true,
				\CoreLibs\DB\IO::DEFAULT_MAX_QUERY_CALL,
				// expected warning
				'',
				// expected error
				'',
			],
			'set to -1 with warning' => [
				-1,
				true,
				-1,
				// warning 50
				'50',
				'',
			],
			'set to 0 with error' => [
				0,
				false,
				\CoreLibs\DB\IO::DEFAULT_MAX_QUERY_CALL,
				'',
				'51',
			],
			'set to -2 with error' => [
				-2,
				false,
				\CoreLibs\DB\IO::DEFAULT_MAX_QUERY_CALL,
				'',
				'51',
			],
			'set to valid value' => [
				10,
				true,
				10,
				'',
				'',
			]
		];
	}

	/**
	 * Test max query call set and get flow with warging/errors
	 *
	 * @covers ::dbSetMaxQueryCall
	 * @covers ::dbGetMaxQueryCall
	 * @dataProvider maxQueryCallProvider
	 * @testdox Set max query call with $max_calls out with $expected_flag and $expected_max_calls (Warning: $warning/Error: $error) [$_dataName]
	 *
	 * @param integer|null $max_calls
	 * @param boolean $expected_flag
	 * @param integer $expected_max_calls
	 * @param string $warning
	 * @param string $error
	 * @return void
	 */
	public function testMaxQueryCall(
		?int $max_calls,
		bool $expected_flag,
		int $expected_max_calls,
		string $warning,
		string $error
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$this->assertEquals(
			$expected_flag,
			// TODO special test with null call too
			$max_calls === null ?
				$db->dbSetMaxQueryCall() :
				$db->dbSetMaxQueryCall($max_calls)
		);
		$this->assertEquals(
			$expected_max_calls,
			$db->dbGetMaxQueryCall()
		);
		// if string for warning or error is not empty check
		$this->assertEquals(
			$warning,
			$db->dbGetLastWarning()
		);
		$this->assertEquals(
			$error,
			$db->dbGetLastError()
		);
		$db->dbClose();
	}

	// - all general data from connection array
	//   dbGetSetting (name, user, ecnoding, schema, host, port, ssl, debug, password)

	/**
	 * returns ALL connections sets as a group with
	 * conneciton name on pos 0 and the connection settings on pos 1
	 *
	 * @return array
	 */
	public function connectionCompleteProvider(): array
	{
		$connections = [];
		foreach (self::$db_config as $connection => $settings) {
			$connections['DB Connection: ' . $connection] = [
				$connection,
				$settings,
			];
		}
		return $connections;
	}

	/**
	 * Test connection array settings return call
	 *
	 * @covers ::dbGetSetting
	 * @dataProvider connectionCompleteProvider
	 * @testdox Get settings for connection $connection [$_dataName]
	 *
	 * @param string $connection,
	 * @param array $settings
	 * @return void
	 */
	public function testGetSetting(string $connection, array $settings): void
	{
		// if settings are all empty -> assume exception
		if (empty($settings['db_name']) && empty($settings['db_user'])) {
			$this->expectException('RuntimeException');
		}
		$db = new \CoreLibs\DB\IO(
			self::$db_config[$connection],
			self::$log
		);

		// each must match
		foreach (
			[
				'name' => 'db_name',
				'user' => 'db_user',
				'encoding' => 'db_encoding',
				'schema' => 'db_schema',
				'host' => 'db_host',
				'port' => 'db_port',
				'ssl' => 'db_ssl',
				'password' => '***',
			] as $read => $compare
		) {
			$this->assertEquals(
				$read == 'password' ? $compare : $settings[$compare],
				$db->dbGetSetting($read)
			);
		}

		$db->dbClose();
	}

	// - test boolean convert
	//   dbBoolean

	/**
	 * test list for dbBoolean
	 *
	 * @return array
	 */
	public function booleanProvider(): array
	{
		// 0: set
		// 1: reverse flag
		// 2: expected
		return [
			'source "t" to true' => [
				't',
				false,
				true,
			],
			'source "t" to true null flag' => [
				't',
				null,
				true,
			],
			'source "true" to true' => [
				'true',
				false,
				true,
			],
			'source "f" to false' => [
				'f',
				false,
				false,
			],
			'source "false" to false' => [
				'false',
				false,
				false,
			],
			'source anything to true' => [
				'something',
				false,
				true,
			],
			'source empty to false' => [
				'',
				false,
				false,
			],
			'source bool true to "t"' => [
				true,
				true,
				't',
			],
			'source bool false to "f"' => [
				false,
				true,
				'f',
			],
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbBoolean
	 * @dataProvider booleanProvider
	 * @testdox Have $source and convert ($reverse) to $expected [$_dataName]
	 *
	 * @param string|bool $source
	 * @param bool|null $reverse
	 * @param string|bool $expected
	 * @return void
	 */
	public function testDbBoolean($source, ?bool $reverse, $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$this->assertEquals(
			$expected,
			$reverse === null ?
				$db->dbBoolean($source) :
				$db->dbBoolean($source, $reverse)
		);
		$db->dbClose();
	}

	// - test interval/age string conversion to
	//   \CoreLibs\Combined\DateTime::stringToTime/timeStringFormat compatbile
	//   dbTimeFormat

	/**
	 * test list for timestamp parsers
	 *
	 * @return array
	 */
	public function timeFormatProvider(): array
	{
		// 0: set
		// 1: micro seconds flag
		// 2: expected
		return [
			'interval a' => [
				'41 years 9 mons 18 days',
				false,
				'41 years 9 mons 18 days'
			],
			'interval a null micro time' => [
				'41 years 9 mons 18 days',
				null,
				'41 years 9 mons 18 days'
			],
			'interval a-1' => [
				'41 years 9 mons 18 days 12:31:11',
				false,
				'41 years 9 mons 18 days 12h 31m 11s'
			],
			'interval a-2' => [
				'41 years 9 mons 18 days 12:31:11.87418',
				false,
				'41 years 9 mons 18 days 12h 31m 11s'
			],
			'interval a-2-1' => [
				'41 years 9 mons 18 days 12:31:11.87418',
				true,
				'41 years 9 mons 18 days 12h 31m 11s 87418ms'
			],
			'interval a-3' => [
				'41 years 9 mons 18 days 12:00:11',
				false,
				'41 years 9 mons 18 days 12h 11s'
			],
			'interval b' => [
				'1218 days',
				false,
				'1218 days'
			],
			'interval c' => [
				'1 year 1 day',
				false,
				'1 year 1 day'
			],
			'interval d' => [
				'12:00:05',
				false,
				'12h 5s'
			],
			'interval e' => [
				'00:00:00.12345',
				true,
				'12345ms'
			],
			'interval e-1' => [
				'00:00:00',
				true,
				'0s'
			],
			'interval a (negative)' => [
				'-41 years 9 mons 18 days 00:05:00',
				false,
				'-41 years 9 mons 18 days 5m'
			],
		];
	}

	/**
	 * Test parsing of interval strings into human readable format
	 *
	 * @covers ::dbTimeFormat
	 * @dataProvider timeFormatProvider
	 * @testdox Have $source and convert ($show_micro) to $expected [$_dataName]
	 *
	 * @param string $source
	 * @param bool|null $show_micro
	 * @param string $expected
	 * @return void
	 */
	public function testDbTimeFormat(string $source, ?bool $show_micro, string $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$this->assertEquals(
			$expected,
			$show_micro === null ?
				$db->dbTimeFormat($source) :
				$db->dbTimeFormat($source, $show_micro)
		);
		$db->dbClose();
	}

	// - convert PostreSQL arrays into PHP
	//   dbArrayParse

	/**
	 * test list for array convert
	 *
	 * @return array
	 */
	public function arrayProvider(): array
	{
		// 0: postgresql array string
		// 1: php array
		return [
			'array 1' => [
				'{1,2,3,"4 this is shit"}',
				[1, 2, 3, "4 this is shit"]
			],
			'array 2' => [
				'{{1,2,3},{4,5,6}}',
				[[1, 2, 3], [4, 5, 6]]
			],
			'array 3' => [
				'{{{1,2},{3}},{{4},{5,6}}}',
				[[[1, 2], [3]], [[4], [5, 6]]]
			],
			'array 4' => [
				'{dfasdf,"qw,,e{q\"we",\'qrer\'}',
				['dfasdf', 'qw,,e{q"we', 'qrer']
			]
		];
	}

	/**
	 * test convert PostgreSQL array to PHP array
	 *
	 * @covers ::dbArrayParse
	 * @dataProvider arrayProvider
	 * @testdox Input array string $input to $expected [$_dataName]
	 *
	 * @param string $input
	 * @param array|bool $expected
	 * @return void
	 */
	public function testDbArrayParse(string $input, $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$this->assertEquals(
			$expected,
			$db->dbArrayParse($input)
		);
		$db->dbClose();
	}

	// - string escape tests
	//   dbEscapeString, dbEscapeLiteral, dbEscapeIdentifier,

	/**
	 * test list for string encodig
	 *
	 * @return array
	 */
	public function stringProvider(): array
	{
		// 0: input
		// 1: expected string
		// 2: expected literal
		// 3: expected identifier
		return [
			'string normal' => [
				'Foo Bar',
				'Foo Bar',
				'\'Foo Bar\'',
				'"Foo Bar"',
			],
			'string quotes' => [
				'Foo \'" Bar',
				'Foo \'\'" Bar',
				'\'Foo \'\'" Bar\'',
				'"Foo \'"" Bar"',
			],
			'string backslash' => [
				'Foo \ Bar',
				'Foo \ Bar',
				' E\'Foo \\\\ Bar\'',
				'"Foo \ Bar"',
			],
		];
	}

	/**
	 * Check all string escape functions
	 * NOTE:
	 * This depends on the SETTINGS of the DB
	 * The expected setting is the default encoding setting in PostgreSQL
	 * #backslash_quote = safe_encoding
	 * #escape_string_warning = on
	 * TODO: Load current settings from DB and ajust comapre string
	 *
	 * @covers ::dbEscapeString
	 * @covers ::dbEscapeLiteral
	 * @covers ::dbEscapeIdentifier
	 * @dataProvider stringProvider
	 * @testdox Input string $input to $expected [$_dataName]
	 *
	 * @param string $input
	 * @param string $expected
	 * @return void
	 */
	public function testStringEscape(
		string $input,
		string $expected_string,
		string $expected_literal,
		string $expected_identifier
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// print "String: " . $input . "\n";
		// print "Escape String: -" . $db->dbEscapeString($input) . "-\n";
		// print "Escape Literal: -" . $db->dbEscapeLiteral($input) . "-\n";
		// print "Escape Identifier: -" . $db->dbEscapeIdentifier($input) . "-\n";
		$this->assertEquals(
			$expected_string,
			$db->dbEscapeString($input)
		);
		$this->assertEquals(
			$expected_literal,
			$db->dbEscapeLiteral($input)
		);
		$this->assertEquals(
			$expected_identifier,
			$db->dbEscapeIdentifier($input)
		);

		$db->dbClose();
	}

	// - bytea encoding
	//   dbEscapeBytea

	/**
	 * test bytea encoding list
	 *
	 * @return array
	 */
	public function byteaProvider(): array
	{
		// 0: string in
		// 1: bytea expected
		return [
			'standard empty string' => [
				'',
				'\x'
			],
			'random values' => [
				'""9f8a!1012938123712378a../%(\'%)"!"#0"#$%\'"#$00"#$0"#0$0"#$',
				'\x2222396638612131303132393338313233373132333738612e2e2f2528272529222122233022232425272223243030222324302223302430222324'
			],
			'random text' => [
				'string d',
				'\x737472696e672064'
			]
		];
	}

	/**
	 * Test bytea escape
	 * NOTE:
	 * This depends on bytea encoding settings on the server
	 * #bytea_output = 'hex'
	 *
	 * @covers ::dbEscapeBytea
	 * @dataProvider byteaProvider
	 * @testdox Input bytea $input to $expected [$_dataName]
	 *
	 * @param string $input
	 * @param string $expected
	 * @return void
	 */
	public function testByteaEscape(string $input, string $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		$this->assertEquals(
			$expected,
			$db->dbEscapeBytea($input),
			'Assert error to bytea'
		);
		$this->assertEquals(
			$input,
			$db->dbUnescapeBytea($expected),
			'Assert error from bytes'
		);

		$db->dbClose();
	}

	// - string escape catcher
	//   dbSqlEscape

	/**
	 * test list for sql escape function
	 *
	 * @return array
	 */
	public function sqlEscapeProvider(): array
	{
		// 0: data in
		// 1: flag
		// 2: expected output
		return [
			// int (standard)
			'integer value' => [1, 'i', 1,],
			'bad integer value' => ['za', 'i', 0,],
			'empty integer value' => ['', 'i', 'NULL',],
			'null integer value' => [null, 'i', 'NULL',],
			// float (standard)
			'float value' => [1.1, 'f', 1.1,],
			'bad float value' => ['za', 'f', 0,],
			'empty float value' => ['', 'f', 'NULL',],
			'null float value' => [null, 'f', 'NULL',],
			// text (varchar)
			'string value' => ['string value', 't', '\'string value\'',],
			'empty string value' => ['', 't', '\'\'',],
			'null string value' => [null, 't', 'NULL',],
			// text literal (don't worry about ' around string)
			'string value literal' => ['string literal', 'tl', '\'string literal\'',],
			'empty string value literal' => ['', 'tl', '\'\'',],
			'null string value literal' => [null, 'tl', 'NULL',],
			// escape string, but set all empty strings to null ('' is null)
			'string value d' => ['string d', 'd', '\'string d\'',],
			'empty string value d' => ['', 'd', 'NULL',],
			'null string value d' => [null, 'd', 'NULL',],
			// escape literal string, but set all empty strings to null ('' is null)
			'string value literal d' => ['string d', 'dl', '\'string d\'',],
			'empty string value literal d' => ['', 'dl', 'NULL',],
			'null string value literal d' => [null, 'dl', 'NULL',],
			// by bytea
			'string value d' => ['string d', 'by', '\x737472696e672064',],
			'empty string value d' => ['', 'by', 'NULL',],
			'null string value d' => [null, 'by', 'NULL',],
			// b (bool)
			'bool true value' => [true, 'b', '\'t\'',],
			'bool false value' => [false, 'b', '\'f\'',],
			'empty bool value' => ['', 'b', 'NULL',],
			'null bool value' => [null, 'b', 'NULL',],
			// i2 (integer but with 0 instead of NULL for empty)
			'integer2 value' => [1, 'i2', 1,],
			'bad integer2 value' => ['za', 'i2', 0,],
			'empty integer2 value' => ['', 'i2', 0,],
			'null integer2 value' => [null, 'i2', 0,],
		];
	}

	/**
	 * Test for the sql escape/null wrapper function
	 *
	 * @covers ::dbSqlEscape
	 * @dataProvider sqlEscapeProvider
	 * @testdox Input value $input as $flag to $expected [$_dataName]
	 *
	 * @param int|float|string|bool|null $input
	 * @param string $flag
	 * @param int|float|string $expected
	 * @return void
	 */
	public function testSqlEscape($input, string $flag, $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		$this->assertEquals(
			$expected,
			$db->dbSqlEscape($input, $flag)
		);

		$db->dbClose();
	}

	// - show table data
	//   dbShowTableMetaData

	/**
	 * table meta data return test
	 *
	 * @return array
	 */
	public function tableProvider(): array
	{
		// 0: table
		// 1: schema
		// 2: expected array
		return [
			// disable the default tables, they might change
			/* 'table with primary key' => [
				'table_with_primary_key',
				'',
				[]
			],
			'table without primary key' => [
				'table_without_primary_key',
				'public',
				[]
			], */
			'simple table' => [
				'test_meta',
				'',
				[
					'row_1' => [
						'num' => 1,
						'type' => 'varchar',
						'len' => -1,
						'not null' => false,
						'has default' => false,
						'array dims' => 0,
						'is enum' => false,
						'is base' => true,
						'is composite' => false,
						'description' => '',
						'is pseudo' => false
					],
					'row_2' => [
						'num' => 2,
						'type' => 'int4',
						'len' => 4,
						'not null' => false,
						'has default' => false,
						'array dims' => 0,
						'is enum' => false,
						'is base' => true,
						'is composite' => false,
						'description' => '',
						'is pseudo' => false
					]
				]
			],
			'simple table null schema' => [
				'test_meta',
				null,
				[
					'row_1' => [
						'num' => 1,
						'type' => 'varchar',
						'len' => -1,
						'not null' => false,
						'has default' => false,
						'array dims' => 0,
						'is enum' => false,
						'is base' => true,
						'is composite' => false,
						'description' => '',
						'is pseudo' => false
					],
					'row_2' => [
						'num' => 2,
						'type' => 'int4',
						'len' => 4,
						'not null' => false,
						'has default' => false,
						'array dims' => 0,
						'is enum' => false,
						'is base' => true,
						'is composite' => false,
						'description' => '',
						'is pseudo' => false
					]
				]
			],
			'table does not exist' => [
				'non_existing',
				'public',
				false
			],
		];
	}

	/**
	 * test the table meta data return flow
	 *
	 * @covers ::dbShowTableMetaData
	 * @dataProvider tableProvider
	 * @testdox Check table $table in schema $schema with $expected [$_dataName]
	 *
	 * @param string $table
	 * @param string|null $schema
	 * @param array<mixed>|bool $expected
	 * @return void
	 */
	public function testDbShowTableMetaData(string $table, ?string $schema, $expected): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// print "TABLE\n" . print_r($db->dbShowTableMetaData($table, $schema), true) . "\n";

		$this->assertEquals(
			$expected,
			$schema === null ?
				$db->dbShowTableMetaData($table) :
				$db->dbShowTableMetaData($table, $schema)
		);

		$db->dbClose();
	}

	// - db exec test for insert/update/select/etc
	//   dbExec, dbExecParams, dbResetQueryCalled, dbGetQueryCalled

	/**
	 * provide queries with return results
	 *
	 * @return array
	 */
	public function queryDbExecProvider(): array
	{
		// 0: query
		// 1: params, null for force dbEXec
		// 2: optional primary key name, null for empty test
		// 3: expectes result (bool, object)
		// 4: warning
		// 5: error
		// 6: run times, not set is once, true is max + 1
		return [
			// insert
			'table with pk insert' => [
				'INSERT INTO table_with_primary_key (row_date) VALUES (NOW())',
				null,
				'',
				'object',
				'',
				'',
			],
			// insert but null primary key
			'table with pk insert null' => [
				'INSERT INTO table_with_primary_key (row_date) VALUES (NOW())',
				null,
				null,
				'object',
				'',
				'',
			],
			// insert with params
			'table with pk insert params' => [
				'INSERT INTO table_with_primary_key (row_varchar) VALUES ($1)',
				['test'],
				'',
				'object',
				'',
				'',
			],
			// insert with params, null primary key
			'table with pk insert params' => [
				'INSERT INTO table_with_primary_key (row_varchar) VALUES ($1)',
				['test'],
				null,
				'object',
				'',
				'',
			],
			// insert to table with no pk (31?)
			'table with no pk insert' => [
				'INSERT INTO table_without_primary_key (row_date) VALUES (NOW())',
				null,
				'',
				'object',
				'',
				'',
			],
			// insert to table with no pk (31?) with params
			'table with no pk insert params' => [
				'INSERT INTO table_without_primary_key (row_varchar) VALUES ($1)',
				['test'],
				null,
				'object',
				'',
				'',
			],
			// INSERT: returning array possible multi insert (32)
			'table with pk insert multile' => [
				'INSERT INTO table_with_primary_key (row_date) VALUES'
					. '(NOW()), '
					. '(NOW()), '
					. '(NOW()), '
					. '(NOW())',
					null,
				'',
				'object',
				'32',
				'',
			],
			// Skip PK READING
			'table with pk insert and NULL pk name' => [
				'INSERT INTO table_with_primary_key (row_date) VALUES (NOW())',
				null,
				'NULL',
				'object',
				'',
				'',
			],
			// insert with pk set
			'table with pk insert and pk name' => [
				'INSERT INTO table_with_primary_key (row_date) VALUES (NOW())',
				null,
				'table_with_primary_key_id',
				'object',
				'',
				'',
			],
			// update
			'table with pk update' => [
				'UPDATE table_with_primary_key SET row_date = NOW()',
				null,
				'',
				'object',
				'',
				'',
			],
			'table with pk select' => [
				'SELECT * FROM table_with_primary_key',
				null,
				'',
				'object',
				'',
				'',
			],
			// no query set, error 11
			'no query set' => [
				'',
				null,
				'',
				false,
				'',
				'11',
			],
			// wrong params coutn for insert
			'wrong params count' => [
				'INSERT INTO table_with_primary_key (row_varchar) VALUES ($1, $2)',
				['test'],
				null,
				false,
				'',
				'23'
			],
			'wrong params count, null params' => [
				'INSERT INTO table_with_primary_key (row_varchar) VALUES ($1, $2)',
				null,
				null,
				false,
				'',
				'23'
			],
			// no db connection setable (16) [needs Mocking]
			// TODO failed db connection
			// connection busy [async] (41)
			// TODO connection busy
			// same query run too many times (30)
			'same query run too many times' => [
				'SELECT row_date FROM table_with_primary_key',
				null,
				'',
				'object',
				'',
				'30',
				true,
			],
			// execution failed (13)
			'invalid query' => [
				'INVALID',
				null,
				'',
				false,
				'',
				'13'
			],
			// INSERT: cursor invalid for fetch PK (34) [unreachable code]
			// INSERT: returning has no data (33)
			// invalid RETURNING columns
			// NOTE: After an error was encountered, queries after this
			//       will return a true connection busy although it was error
			//       https://bugs.php.net/bug.php?id=36469
			//       FIX with socket check type
			'invalid returning' => [
				'INSERT INTO table_with_primary_key (row_date) VALUES (NOW()) RETURNING invalid',
				null,
				'',
				false,
				'',
				'13'
			],
		];
	}

	/**
	 * pure dbExec/dbExecParams checker
	 * does not check __dbPostExec run, this will be done in the dbGet* functions
	 * tests (internal read data post exec group)
	 *
	 * @covers ::dbExec
	 * @covers ::dbExecParams
	 * @covers ::dbGetQueryCalled
	 * @covers ::dbResetQueryCalled
	 * @dataProvider queryDbExecProvider
	 * @testdox dbExec $query and pk $pk_name with $expected_return (Warning: $warning/Error: $error) [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed>|null $params
	 * @param string|null $pk_name
	 * @param object|bool $expected_return
	 * @param string $warning
	 * @param string $error
	 * @param bool $run_many_times
	 * @return void
	 */
	public function testDbExec(
		string $query,
		?array $params,
		?string $pk_name,
		$expected_return,
		string $warning,
		string $error,
		bool $run_many_times = false
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// clear any current query
		// $db->dbResetQuery();

		// assert never called query is 0
		$this->assertEquals(
			0,
			$db->dbGetQueryCalled($query),
			'Assert never called query is null'
		);

		// if expected result is not a bool
		// it has to be an object type PgSql\Result

		if (is_bool($expected_return)) {
			// supress ANY errors here
			if ($pk_name === null && $params === null) {
				$result = @$db->dbExec($query);
			} elseif ($params === null) {
				$result = @$db->dbExec($query, $pk_name);
			} elseif ($pk_name === null) {
				$result = @$db->dbExecParams($query, $params);
			} else {
				$result = @$db->dbExecParams($query, $params, $pk_name);
			}
			$this->assertEquals(
				$expected_return,
				$result
			);
		} else {
			if ($pk_name === null && $params === null) {
				$result = $db->dbExec($query);
			} elseif ($params === null) {
				$result = $db->dbExec($query, $pk_name);
			} elseif ($pk_name === null) {
				$result = $db->dbExecParams($query, $params);
			} else {
				$result = $db->dbExecParams($query, $params, $pk_name);
			}
			// if PHP or newer, must be Object PgSql\Result
			$this->assertIsObject(
				$result
			);
			// also check that this is correct instance type
			$this->assertInstanceOf(
				'PgSql\Result',
				$result
			);
		}
		// if we have more than one run time
		// re-run same query and then catch error
		if ($run_many_times) {
			for ($i = 1; $i <= $db->dbGetMaxQueryCall() + 1; $i++) {
				if ($pk_name === null && $params === null) {
					$db->dbExec($query);
				} elseif ($params === null) {
					$db->dbExec($query, $pk_name);
				} elseif ($pk_name === null) {
					$db->dbExecParams($query, $params);
				} else {
					$db->dbExecParams($query, $params, $pk_name);
				}
			}
			if ($pk_name === null && $params === null) {
				$result = $db->dbExec($query);
			} elseif ($params === null) {
				$result = $db->dbExec($query, $pk_name);
			} elseif ($pk_name === null) {
				$result = $db->dbExecParams($query, $params);
			} else {
				$result = $db->dbExecParams($query, $params, $pk_name);
			}
			// will fail now
			$this->assertFalse(
				$result
			);
			// check query called matching
			$current_count = $db->dbGetQueryCalled($query);
			$this->assertEquals(
				$db->dbGetMaxQueryCall() + 1,
				$current_count
			);
			// reset query called and check again
			$this->assertEquals(
				0,
				$db->dbResetQueryCalled($query)
			);
		}

		// if string for warning or error is not empty check
		$this->subAssertErrorTest($db, $warning, $error);

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// - return one database row
	//   dbReturnRow

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function returnRowProvider(): array
	{
		$insert_query = "INSERT INTO table_with_primary_key (row_int, uid) VALUES (1, 'A')";
		$read_query = "SELECT row_int, uid FROM table_with_primary_key WHERE uid = 'A'";
		$read_query_params = "SELECT row_int, uid FROM table_with_primary_key WHERE uid = $1";
		// 0: query
		// 1: params (null) for other
		// 2: flag (assoc)
		// 3: result
		// 4: warning
		// 5: error
		// 6: insert query
		return [
			'valid select' => [
				$read_query,
				null,
				null,
				[
					'row_int' => 1,
					0 => 1,
					'uid' => 'A',
					1 => 'A'
				],
				'',
				'',
				$insert_query,
			],
			'valid select, assoc only false' => [
				$read_query,
				null,
				false,
				[
					'row_int' => 1,
					0 => 1,
					'uid' => 'A',
					1 => 'A'
				],
				'',
				'',
				$insert_query,
			],
			'valid select, assoc only true' => [
				$read_query,
				null,
				true,
				[
					'row_int' => 1,
					'uid' => 'A',
				],
				'',
				'',
				$insert_query,
			],
			// params read
			'valid select, params' => [
				$read_query_params,
				[
					'A'
				],
				true,
				[
					'row_int' => 1,
					'uid' => 'A',
				],
				'',
				'',
				$insert_query
			],
			// errors
			'wrong params count' => [
				$read_query_params,
				[],
				true,
				false,
				'',
				'23',
				$insert_query
			],
			// params, wrong count
			'empty select' => [
				'',
				null,
				null,
				false,
				'',
				'11',
				$insert_query,
			],
			'insert query' => [
				$insert_query,
				null,
				null,
				false,
				'',
				'17',
				$insert_query
			],
			// invalid QUERY
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbReturnRow
	 * @covers ::dbReturnRowParams
	 * @dataProvider returnRowProvider
	 * @testdox dbReturnRow $query and assoc $flag_assoc with $expected (Warning: $warning/Error: $error) [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed>|null $params
	 * @param bool|null $flag_assoc
	 * @param array<mixed>|bool $expected
	 * @param string $warning
	 * @param string $error
	 * @param string $insert_data
	 * @return void
	 */
	public function testDbReturnRow(
		string $query,
		?array $params,
		?bool $flag_assoc,
		$expected,
		string $warning,
		string $error,
		string $insert_data
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		// insert data before we can test, from expected array
		$db->dbExec($insert_data);
		// run
		if ($flag_assoc === null && $params === null) {
			$result = $db->dbReturnRow($query);
		} elseif ($params === null) {
			$result = $db->dbReturnRow($query, $flag_assoc);
		} elseif ($flag_assoc === null) {
			$result = $db->dbReturnRowParams($query, $params);
		} else {
			$result = $db->dbReturnRowParams($query, $params, $flag_assoc);
		}
		// compare
		$this->assertEquals(
			$expected,
			$result
		);
		// get last error/warnings
		// if string for warning or error is not empty check
		$this->subAssertErrorTest($db, $warning, $error);

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// - return all database rows
	//   dbReturnArray

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function returnArrayProvider(): array
	{
		$insert_query = "INSERT INTO table_with_primary_key (row_int, uid) VALUES "
			. "(1, 'A'), (2, 'B')";
		$read_query = "SELECT row_int, uid FROM table_with_primary_key";
		$read_query_params = "SELECT row_int, uid FROM table_with_primary_key WHERE uid = $1";
		// 0: query
		// 1: params (null) for other
		// 2: flag (assoc)
		// 3: result
		// 4: warning
		// 5: error
		// 6: insert query
		return [
			'valid select' => [
				$read_query,
				null,
				null,
				[
					[
						'row_int' => 1,
						'uid' => 'A',
					],
					[
						'row_int' => 2,
						'uid' => 'B',
					],
				],
				'',
				'',
				$insert_query,
			],
			'valid select, assoc ' => [
				$read_query,
				null,
				false,
				[
					[
						'row_int' => 1,
						0 => 1,
						'uid' => 'A',
						1 => 'A'
					],
					[
						'row_int' => 2,
						0 => 2,
						'uid' => 'B',
						1 => 'B'
					],
				],
				'',
				'',
				$insert_query,
			],
			// params read
			'valid select, params' => [
				$read_query_params,
				[
					'A'
				],
				true,
				[
					[
						'row_int' => 1,
						'uid' => 'A',
					]
				],
				'',
				'',
				$insert_query
			],
			// errors
			'wrong params count' => [
				$read_query_params,
				[],
				true,
				false,
				'',
				'23',
				$insert_query
			],
			'empty select' => [
				'',
				null,
				null,
				false,
				'',
				'11',
				$insert_query,
			],
			'insert query' => [
				$insert_query,
				null,
				null,
				false,
				'',
				'17',
				$insert_query
			],
			// invalid QUERY
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbReturnArray
	 * @dataProvider returnArrayProvider
	 * @testdox dbReturnArray $query and assoc $flag_assoc with $expected (Warning: $warning/Error: $error) [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed>|null $params
	 * @param boolean|null $flag_assoc
	 * @param array<mixed>|bool $expected
	 * @param string $warning
	 * @param string $error
	 * @param string $insert_data
	 * @return void
	 */
	public function testDbReturnArray(
		string $query,
		?array $params,
		?bool $flag_assoc,
		$expected,
		string $warning,
		string $error,
		string $insert_data
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		// insert data before we can test, from expected array
		$db->dbExec($insert_data);
		// run
		if ($flag_assoc === null && $params === null) {
			$result = $db->dbReturnArray($query);
		} elseif ($params === null) {
			$result = $db->dbReturnArray($query, $flag_assoc);
		} elseif ($flag_assoc === null) {
			$result = $db->dbReturnArrayParams($query, $params);
		} else {
			$result = $db->dbReturnArrayParams($query, $params, $flag_assoc);
		}
		// compare
		$this->assertEquals(
			$expected,
			$result
		);
		// get last error/warnings
		// if string for warning or error is not empty check
		$this->subAssertErrorTest($db, $warning, $error);

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// - loop data return flow
	//   dbReturn, dbCacheReset, dbCursorPos, dbCursorNumRows, dbGetCursorExt

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function dbReturnProvider(): array
	{
		$insert_query = "INSERT INTO table_with_primary_key (row_int, uid) VALUES "
			. "(1, 'A'), (2, 'B')";
		$read_query = "SELECT row_int, uid FROM table_with_primary_key";
		$read_query_params = "SELECT row_int, uid FROM table_with_primary_key WHERE uid = $1";
		$row_a = [
			'row_int' => 1,
			0 => 1,
			'uid' => 'A',
			1 => 'A'
		];
		$row_a_assoc = [
			'row_int' => 1,
			'uid' => 'A',
		];
		$row_b = [
			'row_int' => 2,
			0 => 2,
			'uid' => 'B',
			1 => 'B'
		];
		$row_b_assoc = [
			'row_int' => 2,
			'uid' => 'B',
		];
		// 0: read query
		// 1: params, null for not used
		// 2: reset flag, null for default
		// 3: assoc flag, null for default
		// 4: expected return (cursor_ext/data)
		// 5: step through, or normal loop read
		// 6: cursor ext compare array
		// 7*: first only, extended cursor (for each step) [not implemented yet]
		// 8: warning
		// 9: error
		// 10: insert data
		return [
			// *** READ STEP BY STEP
			// default cache: USE_CACHE
			'valid select, default cache settings (NO_CACHE)' => [
				// 0-3
				$read_query,
				null,
				null,
				null,
				// 4
				$row_a,
				// 5
				true,
				// 6
				// check cursor_ext
				[
					'cursor' => 'PgSql\Result',
					'data' => [],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 1,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 1,
					'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
					'assoc_flag' => false,
					'cached' => false,
					'finished' => false,
					'read_finished' => false,
					'db_read_finished' => false,
				],
				// 7
				// extended cursor per step (first only true)
				[
					// second row
					[
						'data' => [
							0 => $row_b,
						],
						'cursor' => [
							'cursor' => 'PgSql\Result',
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 2,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
							'assoc_flag' => false,
							'cached' => false,
							'finished' => false,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					],
					// end row, false
					[
						'data' => false,
						'cursor' => [
							'cursor' => 1,
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 0,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
							'assoc_flag' => false,
							'cached' => false,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					]
				],
				// warn/error
				'',
				'',
				// insert
				$insert_query
			],
			'valid select, use cache, assoc only' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::USE_CACHE,
				true,
				$row_a_assoc,
				true,
				// is same as default
				[
					'cursor' => 'PgSql\Result',
					'data' => [
						0 => $row_a_assoc,
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 1,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 1,
					'cache_flag' => \CoreLibs\DB\IO::USE_CACHE,
					'assoc_flag' => true,
					'cached' => true,
					'finished' => false,
					'read_finished' => false,
					'db_read_finished' => false,
				],
				[
					// second row
					[
						'data' => [
							0 => $row_b_assoc,
						],
						'cursor' => [
							'cursor' => 'PgSql\Result',
							'data' => [
								0 => $row_a_assoc,
								1 => $row_b_assoc,
							],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 2,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::USE_CACHE,
							'assoc_flag' => true,
							'cached' => true,
							'finished' => false,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					],
					// end row, false
					[
						'data' => false,
						'cursor' => [
							'cursor' => 1,
							'data' => [
								0 => $row_a_assoc,
								1 => $row_b_assoc,
							],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 0,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::USE_CACHE,
							'assoc_flag' => true,
							'cached' => true,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					]
				],
				'',
				'',
				$insert_query
			],
			'valid select, read new, assoc only' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::READ_NEW,
				true,
				$row_a_assoc,
				true,
				[
					'cursor' => 'PgSql\Result',
					'data' => [
						0 => $row_a_assoc,
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 1,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 1,
					'cache_flag' => \CoreLibs\DB\IO::READ_NEW,
					'assoc_flag' => true,
					'cached' => true,
					'finished' => false,
					'read_finished' => false,
					'db_read_finished' => false,
				],
				[
					// second row
					[
						'data' => [
							0 => $row_b_assoc,
						],
						'cursor' => [
							'cursor' => 'PgSql\Result',
							'data' => [
								0 => $row_a_assoc,
								1 => $row_b_assoc,
							],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 2,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::READ_NEW,
							'assoc_flag' => true,
							'cached' => true,
							'finished' => false,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					],
					// end row, false
					[
						'data' => false,
						'cursor' => [
							'cursor' => 1,
							'data' => [
								0 => $row_a_assoc,
								1 => $row_b_assoc,
							],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 0,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::READ_NEW,
							'assoc_flag' => true,
							'cached' => true,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					]
				],
				'',
				'',
				$insert_query
			],
			'valid select, clear cache, assoc only' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::CLEAR_CACHE,
				true,
				$row_a_assoc,
				true,
				[
					'cursor' => 'PgSql\Result',
					'data' => [
						0 => $row_a_assoc,
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 1,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 1,
					'cache_flag' => \CoreLibs\DB\IO::CLEAR_CACHE,
					'assoc_flag' => true,
					'cached' => true,
					'finished' => false,
					'read_finished' => false,
					'db_read_finished' => false,
				],
				[
					// second row
					[
						'data' => [
							0 => $row_b_assoc,
						],
						'cursor' => [
							'cursor' => 'PgSql\Result',
							'data' => [
								0 => $row_a_assoc,
								1 => $row_b_assoc,
							],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 2,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::CLEAR_CACHE,
							'assoc_flag' => true,
							'cached' => true,
							'finished' => false,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					],
					// end row, false
					[
						'data' => false,
						'cursor' => [
							'cursor' => 1,
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 0,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::CLEAR_CACHE,
							'assoc_flag' => true,
							'cached' => false,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					]
				],
				'',
				'',
				$insert_query
			],
			'valid select, no cache, assoc only' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::NO_CACHE,
				true,
				$row_a_assoc,
				true,
				[
					'cursor' => 'PgSql\Result',
					'data' => [],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 1,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 1,
					'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
					'assoc_flag' => true,
					'cached' => false,
					'finished' => false,
					'read_finished' => false,
					'db_read_finished' => false,
				],
				[
					// second row
					[
						'data' => [
							0 => $row_b_assoc,
						],
						'cursor' => [
							'cursor' => 'PgSql\Result',
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 2,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
							'assoc_flag' => true,
							'cached' => false,
							'finished' => false,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					],
					// end row, false
					[
						'data' => false,
						'cursor' => [
							'cursor' => 1,
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 2,
							'pos' => 0,
							'query' => $read_query,
							'params' => [],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
							'assoc_flag' => true,
							'cached' => false,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					]
				],
				'',
				'',
				$insert_query
			],
			'valid select params, no cache, assoc only' => [
				// 0-3
				$read_query_params,
				['A'],
				\CoreLibs\DB\IO::NO_CACHE,
				true,
				// 4
				$row_a_assoc,
				// 5
				true,
				// 6 cursor
				[
					'cursor' => 'PgSql\Result',
					'data' => [],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 1,
					'pos' => 1,
					'query' => $read_query_params,
					'params' => ['A'],
					'read_rows' => 1,
					'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
					'assoc_flag' => true,
					'cached' => false,
					'finished' => false,
					'read_finished' => true,
					'db_read_finished' => true,
				],
				// 7 extended cursor
				[
					// second row/last row
					[
						'data' => [
							0 => $row_b_assoc,
						],
						'cursor' => [
							'cursor' => 'PgSql\Result',
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 1,
							'pos' => 2,
							'query' => $read_query_params,
							'params' => ['A'],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
							'assoc_flag' => true,
							'cached' => false,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					],
					// end row, false
					[
						'data' => false,
						'cursor' => [
							'cursor' => 1,
							'data' => [],
							'field_names' => [
								'row_int',
								'uid'
							],
							'field_types' => [
								'int4',
								'varchar'
							],
							'num_fields' => 2,
							'num_rows' => 1,
							'pos' => 0,
							'query' => $read_query_params,
							'params' => ['A'],
							'read_rows' => 2,
							'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
							'assoc_flag' => true,
							'cached' => false,
							'finished' => true,
							'read_finished' => true,
							'db_read_finished' => true,
						]
					]
				],
				// 8-10
				'',
				'',
				$insert_query
			],
			// *** READ STEP BY STEP, ERROR TRIGGER
			'empty select error' => [
				// 0-3
				'',
				null,
				null,
				null,
				// 4
				false,
				// 5
				true,
				// 6
				[],
				// 7
				[],
				// 8-10
				'',
				'11',
				$insert_query,
			],
			'params count wrong' => [
				// 0-3
				$read_query_params,
				[],
				null,
				null,
				// 4
				false,
				// 5
				true,
				// 6
				[],
				// 7
				[],
				// 8-10
				'',
				'23',
				$insert_query,
			],
			'insert query error' => [
				$insert_query,
				null,
				null,
				null,
				false,
				true,
				[],
				[],
				'',
				'17',
				$insert_query
			],
			// *** READ AS LOOP
			// from here on a complex read all full tests
			'valid select, full read, default cache settings (NO CACHE)' => [
				$read_query,
				null,
				null,
				null,
				[$row_a, $row_b,],
				false,
				[
					'cursor' => 1,
					'data' => [],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 0,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 2,
					'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
					'assoc_flag' => false,
					'cached' => false,
					'finished' => true,
					'read_finished' => true,
					'db_read_finished' => true,
				],
				[],
				'',
				'',
				$insert_query
			],
			// USE CACHE
			'valid select, full read, USE CACHE' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::USE_CACHE,
				null,
				[$row_a, $row_b,],
				false,
				[
					'cursor' => 1,
					'data' => [
						0 => $row_a,
						1 => $row_b,
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 0,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 2,
					'cache_flag' => \CoreLibs\DB\IO::USE_CACHE,
					'assoc_flag' => false,
					'cached' => true,
					'finished' => true,
					'read_finished' => true,
					'db_read_finished' => true,
				],
				[],
				'',
				'',
				$insert_query
			],
			// READ_NEW
			'valid select, full read, READ NEW' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::READ_NEW,
				null,
				[$row_a, $row_b,],
				false,
				[
					'cursor' => 1,
					'data' => [
						0 => $row_a,
						1 => $row_b,
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 0,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 2,
					'cache_flag' => \CoreLibs\DB\IO::READ_NEW,
					'assoc_flag' => false,
					'cached' => true,
					'finished' => true,
					'read_finished' => true,
					'db_read_finished' => true,
				],
				[],
				'',
				'',
				$insert_query
			],
			// CLEAR_CACHE
			'valid select, full read, CLEAR CACHE' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::CLEAR_CACHE,
				null,
				[$row_a, $row_b,],
				false,
				[
					'cursor' => 1,
					'data' => [
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 0,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 2,
					'cache_flag' => \CoreLibs\DB\IO::CLEAR_CACHE,
					'assoc_flag' => false,
					'cached' => false,
					'finished' => true,
					'read_finished' => true,
					'db_read_finished' => true,
				],
				[],
				'',
				'',
				$insert_query
			],
			'valid select, full read, NO CACHE' => [
				$read_query,
				null,
				\CoreLibs\DB\IO::NO_CACHE,
				null,
				[$row_a, $row_b,],
				false,
				[
					'cursor' => 1,
					'data' => [
					],
					'field_names' => [
						'row_int',
						'uid'
					],
					'field_types' => [
						'int4',
						'varchar'
					],
					'num_fields' => 2,
					'num_rows' => 2,
					'pos' => 0,
					'query' => $read_query,
					'params' => [],
					'read_rows' => 2,
					'cache_flag' => \CoreLibs\DB\IO::NO_CACHE,
					'assoc_flag' => false,
					'cached' => false,
					'finished' => true,
					'read_finished' => true,
					'db_read_finished' => true,
				],
				[],
				'',
				'',
				$insert_query
			],
		];
	}

	/**
	 * dbReturn cursor extended checks
	 *
	 * @param  \CoreLibs\DB\IO   $db
	 * @param  string            $query
	 * @param  array<mixed>|null $params
	 * @param  array             $cursor_ext_checks
	 * @return void
	 */
	private function subAssertCursorExtTestDbReturnFunction(
		\CoreLibs\DB\IO $db,
		string $query,
		?array $params,
		array $cursor_ext_checks
	): void {
		// cursor check
		if (
			empty($db->dbGetLastWarning()) &&
			empty($db->dbGetLastError()) &&
			count($cursor_ext_checks)
		) {
			if ($params === null) {
				$cursor_ext = $db->dbGetCursorExt($query);
			} else {
				$cursor_ext = $db->dbGetCursorExt($query, $params);
			}
			foreach ($cursor_ext_checks as $key => $expected) {
				if ($key != 'cursor') {
					$this->assertEquals(
						$expected,
						$cursor_ext[$key],
						'assert equal cursor ext for ' . $key
					);
				} else {
					// for int, it has to be one
					// else depends on PHP version, either object or REsource
					if (is_int($expected)) {
						$this->assertEquals(
							1,
							$cursor_ext[$key],
							'assert equal cursor ext cursor int 1'
						);
					} else {
						$this->assertIsObject(
							$cursor_ext[$key],
							'assert is object cursor ext cursor'
						);
						// also check that this is correct instance type
						$this->assertInstanceOf(
							$expected,
							$cursor_ext[$key],
							'assert is instance of cursor ext cursor'
						);
					}
				}
			}
		}
	}

	/**
	 * Undocumented function
	 *
	 * @param  \CoreLibs\DB\IO    $db
	 * @param  string             $query
	 * @param  array<mixed>|null  $params
	 * @param  int|null           $flag_cache
	 * @param  bool|null          $flag_assoc
	 * @return array<mixed>|false
	 */
	private function subDbReturnCall(
		\CoreLibs\DB\IO $db,
		string $query,
		?array $params,
		?int $flag_cache,
		?bool $flag_assoc,
	): array|false {
		if ($flag_cache === null && $flag_assoc === null && $params === null) {
			$result = $db->dbReturn($query);
		} elseif ($flag_assoc === null && $params === null) {
			$result = $db->dbReturn($query, $flag_cache);
		} elseif ($params === null) {
			$result = $db->dbReturn($query, $flag_cache, $flag_assoc);
		} elseif ($flag_cache === null && $flag_assoc === null) {
			$result = $db->dbReturnParams($query, $params);
		} elseif ($flag_assoc === null) {
			$result = $db->dbReturnParams($query, $params, $flag_cache);
		} else {
			$result = $db->dbReturnParams($query, $params, $flag_cache, $flag_assoc);
		}
		return $result;
	}

	/**
	 * dbReturn Function Test
	 *
	 * @covers ::dbReturn
	 * @covers ::dbReturnParams
	 * @covers ::dbCacheReset
	 * @covers ::dbGetCursorExt
	 * @covers ::dbCursorPos
	 * @covers ::dbCursorNumRows
	 * @dataProvider dbReturnProvider
	 * @testdox dbReturn Read First $read_first_only only and cache $flag_cache and assoc $flag_assoc with (Warning: $warning/Error: $error) [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed>|null $params
	 * @param integer|null $flag_cache
	 * @param boolean|null $flag_assoc
	 * @param array<mixed>|bool $expected
	 * @param bool $read_first_only
	 * @param array<mixed> $cursor_ext_checks
	 * @param array<mixed> $cursor_ext_checks_step
	 * @param string $warning
	 * @param string $error
	 * @param string $insert_data
	 * @return void
	 */
	public function testDbReturnFunction(
		string $query,
		?array $params,
		?int $flag_cache,
		?bool $flag_assoc,
		$expected,
		bool $read_first_only,
		array $cursor_ext_checks,
		array $cursor_ext_checks_step,
		string $warning,
		string $error,
		string $insert_data
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		// insert data before we can test, from expected array
		$db->dbExec($insert_data);

		// all checks below
		if ($read_first_only === true) {
			// run query
			$result = $this->subDbReturnCall(
				$db,
				$query,
				$params,
				$flag_cache,
				$flag_assoc
			);
			// simple assert first read, then discard result
			// compare data
			$this->assertEquals(
				$expected,
				$result,
				'Assert dbReturn first only equal'
			);
			$this->subAssertErrorTest($db, $warning, $error);
			$this->subAssertCursorExtTestDbReturnFunction(
				$db,
				$query,
				$params,
				$cursor_ext_checks
			);
			// extended checks. read until end of data and check per steck cursor data
			foreach ($cursor_ext_checks_step as $_cursor_ext_checks) {
				// each step matches a read step
			}
		} else {
			// read all, and then do result compare
			// do cache reset test
			$data = [];
			$pos = 0;
			while (
				is_array(
					$res = $this->subDbReturnCall(
						$db,
						$query,
						$params,
						$flag_cache,
						$flag_assoc
					)
				)
			) {
				$data[] = $res;
				$pos++;
				// check cursor pos
				$this->assertEquals(
					$pos,
					($params === null ?
						$db->dbGetCursorPos($query) :
						$db->dbGetCursorPos($query, $params)
					),
					'Assert dbReturn pos'
				);
			}
			// does count match for returned data and the cursor num rows
			$this->assertEquals(
				count($data),
				($params === null ?
					$db->dbGetCursorNumRows($query) :
					$db->dbGetCursorNumRows($query, $params)
				),
				'Assert dbReturn num rows'
			);
			// run cursor ext checks after first run
			$this->subAssertCursorExtTestDbReturnFunction(
				$db,
				$query,
				$params,
				$cursor_ext_checks
			);
			// does data match
			// try get cursor data for non existing, must be null
			$this->assertNull(
				$db->dbGetCursorExt($query, [], 'nonexistingfield')
			);
			// does reset data work, query cursor must be null
			if ($params === null) {
				$db->dbCacheReset($query);
			} else {
				$db->dbCacheReset($query, $params);
			}
			$this->assertNull(
				($params === null ?
					$db->dbGetCursorExt($query) :
					$db->dbGetCursorExt($query, $params)
				),
				'Assert dbReturn reset cache'
			);
			// New run after reset
			// for read all, test that two reads result in same data
			$data = [];
			$pos = 0;
			while (
				is_array(
					$res = $this->subDbReturnCall(
						$db,
						$query,
						$params,
						$flag_cache,
						$flag_assoc
					)
				)
			) {
				$data[] = $res;
				$pos++;
				// check cursor pos
				$this->assertEquals(
					$pos,
					($params === null ?
						$db->dbGetCursorPos($query) :
						$db->dbGetCursorPos($query, $params)
					),
					'Assert dbReturn double read 1 pos: ' . $flag_cache
				);
			}
			$this->subAssertCursorExtTestDbReturnFunction(
				$db,
				$query,
				$params,
				$cursor_ext_checks
			);
			$data_second = [];
			$pos_second = 0;
			while (
				is_array(
					$res = $this->subDbReturnCall(
						$db,
						$query,
						$params,
						$flag_cache,
						$flag_assoc
					)
				)
			) {
				$data_second[] = $res;
				$pos_second++;
				// check cursor pos
				$this->assertEquals(
					$pos_second,
					($params === null ?
						$db->dbGetCursorPos($query) :
						$db->dbGetCursorPos($query, $params)
					),
					'Assert dbReturn double read 2 pos: ' . $flag_cache
				);
			}
			$this->assertEquals(
				$data,
				$data_second,
				'Assert first and second run are equal: return data'
			);
			$this->assertEquals(
				$pos,
				$pos_second,
				'Assert first and second run are equal: count'
			);
			// run cursor ext checks after second run
			$this->subAssertCursorExtTestDbReturnFunction(
				$db,
				$query,
				$params,
				$cursor_ext_checks
			);
		}

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// - prepared query execute
	//   dbPrepare, dbExecute, dbFetchArray, dbGetPrepareCursorValue

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function preparedProvider(): array
	{
		$insert_query = "INSERT INTO table_with_primary_key (row_int, uid) VALUES "
			. "(1, 'A'), (2, 'B')";
		$read_query = "SELECT row_int, uid FROM table_with_primary_key";
		// 0: statement name
		// 1: query to prepare
		// 2: primary key name: null for default run
		// 3: arguments for query (single array for all)
		// 4: expected prepare return
		// 5: prepare warning
		// 6: prepare error
		// 7: expected execute return
		// 8: execute warning
		// 9: execute error
		// 11: read query (if insert/update)
		// 11: execute data to check (array)
		// 12: insert data
		// 13: prepated cursor array data match values
		return [
			// insert
			'prepare query insert' => [
				// base statements 0-3
				'insert',
				"INSERT INTO table_with_primary_key (row_int, uid) VALUES "
					. "($1, $2)",
				null,
				[990, 'TEST A'],
				// prepare (4-6)
				'result', '', '',
				// execute
				'result', '', '',
				// check query and compare data (for insert/update)
				$read_query,
				[
					[
						'row_int' => 990,
						'uid' => 'TEST A',
					],
				],
				// insert data (for select)
				'',
				// get prepared data
				[
					'pk_name' => 'table_with_primary_key_id',
					'count' => 2,
					'query' =>  'INSERT INTO table_with_primary_key (row_int, uid) '
						. 'VALUES ($1, $2) RETURNING table_with_primary_key_id',
					'returning_id' => true,
					'placeholder_converted' => [],
				],
			],
			// update
			'prepare query update' => [
				'update',
				"UPDATE table_with_primary_key SET "
					. "row_int = $1, "
					. "row_varchar = $2 "
					. "WHERE uid = $3",
				null,
				[550, 'I AM NEW TEXT', 'TEST A'],
				//
				'result', '', '',
				//
				'result', '', '',
				//
				"SELECT row_int, row_varchar FROM table_with_primary_key "
					. "WHERE uid = 'TEST A'",
				[
					[
						'row_int' => 550,
						'row_varchar' => 'I AM NEW TEXT',
					],
				],
				//
				"INSERT INTO table_with_primary_key (row_int, uid) VALUES "
					. "(111, 'TEST A')",
				//
				[
					'pk_name' => '',
					'count' => 3,
					'query' => 'UPDATE table_with_primary_key SET row_int = $1, '
						. 'row_varchar = $2 WHERE uid = $3',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// select
			'prepare select query' => [
				'select',
				$read_query
					. " WHERE uid = $1",
				null,
				['A'],
				//
				'result', '', '',
				// execute here needs to read too
				'result', '', '',
				//
				'',
				[
					[
						'row_int' => 1,
						'uid' => 'A',
					],
				],
				//
				$insert_query,
				//
				[
					'pk_name' => '',
					'count' => 1,
					'query' => 'SELECT row_int, uid FROM table_with_primary_key WHERE uid = $1',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// any query but with no parameters
			'prepare select query no parameter' => [
				'select_noparam',
				$read_query,
				null,
				null,
				//
				'result', '', '',
				// execute here needs to read too
				'result', '', '',
				//
				'',
				[
					[
						'row_int' => 1,
						'uid' => 'A',
					],
					[
						'row_int' => 2,
						'uid' => 'B',
					],
				],
				//
				$insert_query,
				//
				[
					'pk_name' => '',
					'count' => 0,
					'query' => 'SELECT row_int, uid FROM table_with_primary_key',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// no statement name (25)
			'empty statement' => [
				'',
				'SELECT',
				null,
				[],
				//
				false, '', '25',
				//
				false, '', '25',
				//
				'',
				[],
				//
				'',
				//
				[
					'pk_name' => '',
					'count' => 0,
					'query' => '',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// no query (prepare 11)
			// no prepared cursor found with statement name (execute 24)
			'empty query' => [
				'Empty Query',
				'',
				null,
				[],
				//
				false, '', '11',
				//
				false, '', '24',
				//
				'',
				[],
				//
				'',
				//
				[
					'pk_name' => '',
					'count' => 0,
					'query' => '',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// no db connection (prepare/execute 16)
			// TODO no db connection test
			// connection busy (prepare/execute 41)
			// TODO connection busy test
			// query could not be prepare (prepare 21)
			// TODO query could not be prepared test
			// some query with same statement name exists (prepare W20)
			'prepare query with same statement name' => [
				'double',
				$read_query,
				null,
				null,
				// warning: 20
				true, '20', '',
				//
				'result', '', '',
				// no query but data for data only compare
				'',
				[],
				//
				$insert_query,
				//
				[
					'pk_name' => '',
					'count' => 0,
					'query' => 'SELECT row_int, uid FROM table_with_primary_key',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// prepare with different statement name
			'prepare query with same statement name, different query' => [
				'double_error',
				$read_query,
				// primary key
				null,
				// arguments (none)
				null,
				// expected return false, warning: no, error: 26
				false, '', '26',
				// return expected, warning, error
				'', '', '',
				// dummy query for second prepare with wrong query
				$read_query . ' WHERE uid = $3',
				[],
				//
				$insert_query,
				//
				[
					'pk_name' => '',
					'count' => 0,
					'query' => 'SELECT row_int, uid FROM table_with_primary_key',
					'returning_id' => false,
					'placeholder_converted' => [],
				],
			],
			// insert wrong data count compared to needed (execute 23)
			'wrong parmeter count' => [
				'wrong_param_count',
				"INSERT INTO table_with_primary_key (row_int, uid) VALUES "
					. "($1, $2)",
				null,
				[],
				//
				'result', '', '',
				//
				false, '', '23',
				//
				'',
				[],
				//
				'',
				//
				[
					'pk_name' => 'table_with_primary_key_id',
					'count' => 2,
					'query' => 'INSERT INTO table_with_primary_key (row_int, uid) VALUES '
						. '($1, $2) RETURNING table_with_primary_key_id',
					'returning_id' => true,
					'placeholder_converted' => [],
				],
			],
			// execute does not return a result (22)
			// TODO execute does not return a result
			// TODO prepared statement with placeholder params auto convert
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbPrepare
	 * @covers ::dbExecute
	 * @covers ::dbFetchArray
	 * @covers ::dbGetPrepareCursorValue
	 * @dataProvider preparedProvider
	 * @testdox prepared query $stm_name with $expected_prepare (warning $warning_prepare/error $error_prepare) and $expected_execute (warning $warning_execute/error $error_execute) [$_dataName]
	 *
	 * @param string $stm_name
	 * @param string $query
	 * @param string|null $pk_name
	 * @param array|null $query_data
	 * @param bool|string $expected_prepare
	 * @param string $warning_prepare
	 * @param string $error_prepare
	 * @param bool|string $expected_execute
	 * @param string $warning_execute
	 * @param string $error_execute
	 * @param string $expected_data_query
	 * @param array $expected_data
	 * @param string $insert_data
	 * @param array $prepare_cursor
	 * @return void
	 */
	public function testDbPrepared(
		string $stm_name,
		string $query,
		?string $pk_name,
		?array $query_data,
		$expected_prepare,
		string $warning_prepare,
		string $error_prepare,
		$expected_execute,
		string $warning_execute,
		string $error_execute,
		string $expected_data_query,
		array $expected_data,
		string $insert_data,
		array $prepare_cursor,
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		// insert data before we can test, from expected array
		if (!empty($insert_data)) {
			$db->dbExec($insert_data);
		}

		// test prepare
		$prepare_result = $pk_name === null ?
			$db->dbPrepare($stm_name, $query) :
			$db->dbPrepare($stm_name, $query, $pk_name);
		// if warning is 20, call prepare again
		if ($warning_prepare == '20') {
			$prepare_result = $pk_name === null ?
				$db->dbPrepare($stm_name, $query) :
				$db->dbPrepare($stm_name, $query, $pk_name);
		}
		if ($error_prepare == '26') {
			$prepare_result = $db->dbPrepare($stm_name, $expected_data_query);
		}
		// if result type, or if forced bool
		if (is_string($expected_prepare) && $expected_prepare == 'result') {
			// if PHP or newer, must be Object PgSql\Result
			$this->assertIsObject(
				$prepare_result
			);
			// also check that this is correct instance type
			$this->assertInstanceOf(
				'PgSql\Result',
				$prepare_result
			);
		} else {
			$this->assertEquals(
				$expected_prepare,
				$prepare_result
			);
		}
		// error/warning check
		$this->subAssertErrorTest($db, $warning_prepare, $error_prepare);

		// for non fail prepare test exec
		// check test result
		if (!$error_prepare) {
			$execute_result = $query_data === null ?
				$db->dbExecute($stm_name) :
				$db->dbExecute($stm_name, $query_data);
			if ($expected_execute == 'result') {
				// if PHP or newer, must be Object PgSql\Result
				$this->assertIsObject(
					$execute_result
				);
				// also check that this is correct instance type
				$this->assertInstanceOf(
					'PgSql\Result',
					$execute_result
				);
				// if this is an select use dbFetchArray to get data and test
			} else {
				$this->assertEquals(
					$expected_execute,
					$execute_result
				);
			}
			// error/warning check
			$this->subAssertErrorTest($db, $warning_execute, $error_execute);
			// now check test result if expected return is result
			if (
				$expected_execute == 'result' &&
				!empty($expected_data_query)
			) {
				// $expected_data_query
				// $expected_data
				$rows = $db->dbReturnArray($expected_data_query);
				$this->assertEquals(
					$expected_data,
					$rows
				);
			}
			if (
				$expected_execute == 'result' &&
				$execute_result !== false &&
				empty($expected_data_query) &&
				count($expected_data)
			) {
				// compare previously read data to compare data
				$compare_data = [];
				// read in the query data
				while (is_array($row = $db->dbFetchArray($execute_result, true))) {
					$compare_data[] = $row;
				}
				$this->assertEquals(
					$expected_data,
					$compare_data
				);
			}

			// check dbGetPrepareCursorValue
			foreach (['pk_name', 'count', 'query', 'returning_id', 'placeholder_converted'] as $key) {
				$this->assertEquals(
					$prepare_cursor[$key],
					$db->dbGetPrepareCursorValue($stm_name, $key),
					'Prepared cursor: ' . $key . ': failed assertion'
				);
			}
		}

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// dedicated error checks for prepare cursor return

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function providerDbGetPrepareCursorValue(): array
	{
		// 1: query (can be empty for do not set)
		// 2: stm name
		// 3: key
		// 4: expected error return
		return [
			'no error' => [
				"SELECT row_int, uid FROM table_with_primary_key",
				'read',
				'pk_name',
				''
			],
			'statement name empty' => [
				'',
				'',
				'',
				'101',
			],
			'key empty' => [
				'',
				'read',
				'',
				'102',
			],
			'key invalid' => [
				'',
				'read',
				'invalid',
				'102',
			],
			'statement name not found' => [
				'',
				'invalid',
				'pk_name',
				'103',
			],
		];
	}

	/**
	 * test return prepare cursor errors
	 *
	 * @covers ::dbGetPrepareCursorValue
	 * @dataProvider providerDbGetPrepareCursorValue
	 * @testdox prepared query $stm_name with $key expect error id $error_id [$_dataName]
	 *
	 * @param string $query
	 * @param string $stm_name
	 * @param string $key
	 * @param string $error_id
	 * @return void
	 */
	public function testDbGetPrepareCursorValue(
		string $query,
		string $stm_name,
		string $key,
		$error_id
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		if (!empty($query)) {
			$db->dbPrepare($stm_name, $query);
			$db->dbExecute($stm_name);
		}
		$db->dbGetPrepareCursorValue($stm_name, $key);
		// match check error
		$last_error = $db->dbGetLastError();
		$this->assertEquals(
			$error_id,
			$last_error,
			'get prepare cursor value error check'
		);
	}

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function providerDbPreparedCursorStatus(): array
	{
		return [
			'empty statement pararm' => [
				'query' => 'SELECT row_int, uid FROM table_with_primary_key',
				'stm_name' => 'test_stm_a',
				'check_stm_name' => '',
				'check_query' => '',
				'expected' => false
			],
			'different stm_name' => [
				'query' => 'SELECT row_int, uid FROM table_with_primary_key',
				'stm_name' => 'test_stm_b',
				'check_stm_name' => 'other_name',
				'check_query' => '',
				'expected' => 0
			],
			'same stm_name' => [
				'query' => 'SELECT row_int, uid FROM table_with_primary_key',
				'stm_name' => 'test_stm_c',
				'check_stm_name' => 'test_stm_c',
				'check_query' => '',
				'expected' => 1
			],
			'same stm_name and query' => [
				'query' => 'SELECT row_int, uid FROM table_with_primary_key',
				'stm_name' => 'test_stm_d',
				'check_stm_name' => 'test_stm_d',
				'check_query' => 'SELECT row_int, uid FROM table_with_primary_key',
				'expected' => 2
			],
			'same stm_name and different query' => [
				'query' => 'SELECT row_int, uid FROM table_with_primary_key',
				'stm_name' => 'test_stm_e',
				'check_stm_name' => 'test_stm_e',
				'check_query' => 'SELECT row_int, uid, row_int FROM table_with_primary_key',
				'expected' => 1
			],
			'insert query test' => [
				'query' => 'INSERT INTO table_with_primary_key (row_int, uid) VALUES ($1, $2)',
				'stm_name' => 'test_stm_f',
				'check_stm_name' => 'test_stm_f',
				'check_query' => 'INSERT INTO table_with_primary_key (row_int, uid) VALUES ($1, $2)',
				'expected' => 2
			]
		];
	}

	/**
	 * test cursor status for prepared statement
	 *
	 * @covers ::dbPreparedCursorStatus
	 * @dataProvider providerDbPreparedCursorStatus
	 * @testdox Check prepared $stm_name ($check_stm_name) status is $expected [$_dataName]
	 *
	 * @param  string   $query
	 * @param  string   $stm_name
	 * @param  string   $check_stm_name
	 * @param  string   $check_query
	 * @param  bool|int $expected
	 * @return void
	 */
	public function testDbPreparedCursorStatus(
		string $query,
		string $stm_name,
		string $check_stm_name,
		string $check_query,
		bool|int $expected
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$db->dbPrepare($stm_name, $query);
		// $db->dbExecute($stm_name);
		$this->assertEquals(
			$expected,
			$db->dbPreparedCursorStatus($check_stm_name, $check_query),
			'check prepared stement cursor status'
		);
		unset($db);
	}

	// - schema set/get tests
	//   dbGetSchema, dbSetSchema

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function schemaProvider(): array
	{
		// 0: db connection
		// 1: schema to set
		// 2: set result
		// 3: set error
		// 4: get result flagged
		// 5: get result DB
		return [
			'schema get check only' => [
				'valid',
				null,
				true,
				'',
				'public',
				'public',
			],
			'new schema set' => [
				'valid',
				'public',
				true,
				'',
				'public',
				'public',
			],
			'no schema set, set new schema' => [
				'valid_no_schema',
				'public',
				true,
				'',
				'public',
				'public',
			],
			'try to set empty schema' => [
				'valid',
				'',
				false,
				'70',
				'public',
				'public',
			],
			// invalid schema (does not throw error)
			'try to set empty schema' => [
				'valid',
				'invalid',
				false,
				'71',
				'public',
				'public',
			],
			// valid different schema
			'try to set new valid schema' => [
				'valid',
				'testschema',
				true,
				'',
				'testschema',
				'testschema',
			]
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbSetSchema
	 * @covers ::dbGetSchema
	 * @dataProvider schemaProvider
	 * @testdox set schema $schema on $connection with $expected_set (error $error_set) and get $expected_get_var/$expected_get_db [$_dataName]
	 *
	 * @param string $connection
	 * @param string|null $schema
	 * @param boolean $expected_set
	 * @param string $error_set
	 * @param string $expected_get_var
	 * @param string $expected_get_db
	 * @return void
	 */
	public function testDbSchema(
		string $connection,
		?string $schema,
		bool $expected_set,
		string $error_set,
		string $expected_get_var,
		string $expected_get_db
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config[$connection],
			self::$log
		);

		// schema is not null, we do set testing
		if ($schema !== null) {
			$result_set = $db->dbSetSchema($schema);
			$last_error = $db->dbGetLastError();
			$this->assertEquals(
				$expected_set,
				$result_set
			);
			// error/warning check
			$this->assertEquals(
				$error_set,
				$last_error,
			);
		}

		// get current set from db
		$result_get_var = $db->dbGetSchema(true);
		$this->assertEquals(
			$expected_get_var,
			$result_get_var
		);
		$result_get_db = $db->dbGetSchema();
		$this->assertEquals(
			$expected_get_db,
			$result_get_db
		);

		// close connection
		$db->dbClose();
	}

	// - check error and warning handling
	//   dbGetCombinedErrorHistory, dbGetLastError, dbGetLastWarning

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function errorHandlingProvider(): array
	{
		// 0: some error call
		// 1: type (error/warning)
		// 2: error/warning code
		// 3: return array matcher (excluding time)
		return [
			'trigger error' => [
				0,
				'error',
				'51',
				[
					'timestamp' => "/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{1,}$/",
					'level' => 'error',
					'id' => '51',
					'error' => 'Max query call needs to be set to at least 1',
					// run:: can be +1 if called in set and not direct
					// 'main::run::run::run::run::run::run::run::runBare::runTest::testDbErrorHandling::dbSetMaxQueryCall
					'source' => "/^(include::)?main::(run::)+runBare::runTest::testDbErrorHandling::dbSetMaxQueryCall$/",
					'pg_error' => '',
					'message' => '',
					'context' => [
						'max_calls' => 0
					]
				]
			],
			'trigger warning' => [
				-1,
				'warning',
				'50',
				[]
			],
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbGetLastError
	 * @covers ::dbGetLastWarning
	 * @covers ::dbGetCombinedErrorHistory
	 * @dataProvider errorHandlingProvider
	 * @testdox error $call_value for type $type with $error_id [$_dataName]
	 *
	 * @param integer $call_value
	 * @param string $type
	 * @param string $error_id
	 * @param array $error_history
	 * @return void
	 */
	public function testDbErrorHandling(
		int $call_value,
		string $type,
		string $error_id,
		array $expected_history
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// trigger the error call
		$db->dbSetMaxQueryCall($call_value);
		if ($type == 'error') {
			$last_error = $db->dbGetLastError();
		} else {
			$last_error = $db->dbGetLastWarning();
		}
		$this->assertEquals(
			$error_id,
			$last_error
		);

		$error_history = $db->dbGetCombinedErrorHistory();
		// pop first error off
		$first_error_element = array_shift($error_history);
		// get first row element
		// comarep all, except timestamp that is a regex
		foreach ($expected_history as $key => $value) {
			// check if starts with / because this is regex (timestamp)
			// if (substr($expected_2, 0, 1) == '/) {
			if (!is_array($value) && strpos($value, '/') === 0) {
				// this is regex
				$this->assertMatchesRegularExpression(
					$value,
					$first_error_element[0][$key]
				);
			} else {
				// assert equal
				$this->assertEquals(
					$value,
					$first_error_element[0][$key],
				);
			}
		}

		// close connection
		$db->dbClose();
	}

	// - encoding settings (exclude encoding test, just set)
	//   dbGetEncoding, dbSetEncoding

	/**
	 * test encoding change list for dbSetEncoding
	 *
	 * @return array
	 */
	public function encodingProvider(): array
	{
		// 0: connection
		// 1: set encoding
		// 2: expected return from set
		// 2: expected to get
		// 3: error id
		return [
			'default set no encoding' => [
				'valid',
				'',
				false,
				// I expect that the default DB is set to UTF8
				'UTF8',
				'',
			],
			'set to Shift JIS' => [
				'valid',
				'ShiftJIS',
				true,
				'SJIS',
				'',
			],
			'set to Invalid' => [
				'valid',
				'Invalid',
				false,
				'UTF8',
				'81',
			],
			'set to empty' => [
				'valid',
				'',
				false,
				'UTF8',
				'80',
			]
		];
	}

	/**
	 * change DB encoding, only function set test, not test of encoding change
	 * TODO: add encoding changed test with DB insert
	 *
	 * @covers ::dbSetEncoding
	 * @covers ::dbGetEncoding
	 * @dataProvider encodingProvider
	 * @testdox Set encoding on $connection to $set_encoding expect $expected_set_flag and $expected_get_encoding [$_dataName]
	 *
	 * @param string $connection
	 * @param string $set_encoding
	 * @param boolean $expected_set_flag
	 * @param string $expected_get_encoding
	 * @return void
	 */
	public function testEncoding(
		string $connection,
		string $set_encoding,
		bool $expected_set_flag,
		string $expected_get_encoding
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config[$connection],
			self::$log
		);
		$this->assertEquals(
			$expected_set_flag,
			// avoid bubbling up error
			@$db->dbSetEncoding($set_encoding)
		);
		// show query
		$this->assertEquals(
			$expected_get_encoding,
			$db->dbGetEncoding()
		);
		// pg_version
		$this->assertEquals(
			$expected_get_encoding,
			$db->dbVersionInfo('client_encoding')
		);
		$db->dbClose();
	}

	// - encoding conversion on read and test encoding conversion on db connection
	//   dbSetToEncoding, dbGetToEncoding
	//   [and test encoding transfer with both types]
	//   dbSetEncoding, dbGetEncoding

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function encodingConversionProvider(): array
	{
		// 0: connection
		// 1: target encoding (or alias)
		// 2: optional name for php for proper alias matching
		// 3: text to check
		return [
			'convert from UTF8 to SJIS' => [
				'valid',
				'SJIS',
				null,
				'日本語カタカナひらがな'
			],
			// SHIFT_JIS_2004/SJIS-win
			// EUC_JP/EUC-JP
			//
		];
	}

	/**
	 * tests actually text conversion and not only just setting
	 * NOTE: database is always stored as UTF8 in our case so all
	 * tests check conversion FROM utf8 to a target.
	 * Also because only SJIS is of interest, only this one is tested
	 * https://www.postgresql.org/docs/current/multibyte.html#MULTIBYTE-CHARSET-SUPPORTED
	 * SHIFT_JIS_2004
	 * SJIS (Mskanji, ShiftJIS, WIN932, Windows932)
	 * EUC_JP
	 * EUC_JIS_2004
	 *
	 * @covers ::dbSetToEncoding
	 * @covers ::dbGetToEncoding
	 * @covers ::dbSetEncoding
	 * @covers ::dbGetEncoding
	 * @dataProvider encodingConversionProvider
	 * @testdox Check encoding on $connection with $encoding [$_dataName]
	 *
	 * @param string $connection
	 * @param string $encoding
	 * @param string|null $encoding_php
	 * @param string $text
	 * @return void
	 */
	public function testEncodingConversion(
		string $connection,
		string $encoding,
		?string $encoding_php,
		string $text
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config[$connection],
			self::$log
		);

		// convert in php unless encoding is the smae
		if (strtolower($encoding) != 'utf8') {
			$encoded = mb_convert_encoding($text, $encoding_php ?? $encoding, 'UTF-8');
		} else {
			$encoded = $text;
		}

		// insert data
		$insert_query = "INSERT INTO table_with_primary_key (row_varchar, uid) VALUES "
			. "(" . $db->dbEscapeLiteral($text) . ", 'A')";
		$db->dbExec($insert_query);
		// for check read
		$read_query = "SELECT row_varchar, uid FROM table_with_primary_key WHERE uid = 'A'";

		// TEST 1 in class
		// test to encoding (conversion with mb_convert_encoding)
		$db->dbSetToEncoding($encoding);
		$this->assertEquals(
			$encoding,
			$db->dbGetToEncoding()
		);
		// read query, check that row_varchar matches
		$row = $db->dbReturnRow($read_query, true);
		$this->assertEquals(
			$encoded,
			$row['row_varchar']
		);
		// reset to encoding to empty
		$db->dbSetToEncoding('');
		// and check
		$this->assertEquals(
			'',
			$db->dbGetToEncoding()
		);

		// TEST 2 DB side
		// same test with setting database encoding
		// if connection encoding differts
		if (strtolower($db->dbGetSetting('encoding')) != strtolower($encoding)) {
			$db->dbSetEncoding($encoding);
		}
		// read from DB and check encoding
		$row = $db->dbReturnRow($read_query, true);
		$this->assertEquals(
			$encoded,
			$row['row_varchar']
		);

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// - get primary key
	//   dbGetReturning, dbGetInsertPK, dbGetInsertPKName

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function primaryKeyProvider(): array
	{
		// 0: insert query add (returning, etc)
		// 1: pk_name, null for default
		// 2: table name
		// 3: primary key or empty if none
		return [
			'normal all auto' => [
				'',
				null,
				'table_with_primary_key',
				'table_with_primary_key_id',
			],
			'table without primary key' => [
				'',
				null,
				'table_without_primary_key',
				''
			],
			// valid primary key name
			'normal, with pk_name' => [
				'',
				'table_with_primary_key_id',
				'table_with_primary_key',
				'table_with_primary_key_id',
			],
			// returning name no pk name
			'normal, with returning' => [
				'RETURNING table_with_primary_key_id',
				null,
				'table_with_primary_key',
				'table_with_primary_key_id',
			],
			// both pk name and returning
			'normal, with returning' => [
				'RETURNING table_with_primary_key_id',
				'table_with_primary_key_id',
				'table_with_primary_key',
				'table_with_primary_key_id',
			],
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbGetInsertPK
	 * @covers ::dbGetInsertPKName
	 * @dataProvider primaryKeyProvider
	 * @testdox Check returning pk $insert with $pk_name [$_dataName]
	 *
	 * @param string $insert
	 * @param string|null $pk_name
	 * @return void
	 */
	public function testGetPrimaryKey(
		string $insert,
		?string $pk_name,
		string $table,
		string $primary_key
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// basic query
		$insert_query = "INSERT INTO " . $db->dbEscapeIdentifier($table)
			. " (uid) "
			. "VALUES ('A') " . $insert;
		$pk_name === null ?
			$db->dbExec($insert_query) :
			$db->dbExec($insert_query, $pk_name);
		// $db_get_returning = $db->dbGetReturning();
		$db_get_insert_pk_name = $db->dbGetInsertPKName();
		$db_get_insert_pk = $db->dbGetInsertPK();

		$read_query = "SELECT "
			. (!empty($primary_key) ?
				$db->dbEscapeIdentifier($primary_key) . ", " : ""
			)
			. "uid "
			. "FROM " . $db->dbEscapeIdentifier($table)
			. " WHERE uid = 'A'";
		$row = $db->dbReturnRow($read_query, true);
		$this->assertEquals(
			$row[$primary_key] ?? null,
			$db_get_insert_pk
		);
		$this->assertEquals(
			$primary_key,
			$db_get_insert_pk_name
		);
		$this->assertTrue(true);


		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// - complex returning data checks
	//   dbGetReturningExt, dbGetReturningArray

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function returningPrvoider(): array
	{
		// NOTE that query can have multiple inserts
		// NOTE if there are different INSERTS before the primary keys
		// will not match anymore. Must be updated by hand
		// IMPORTANT: if this is stand alone the primary key will not match and fail
		$table_with_primary_key_id = 70;
		// 0: query + returning
		// 1: params
		// 1: pk name for db exec
		// 2: key name/value or null (dbGetReturningExt)
		// 3: pos or null (dbGetReturningExt)
		// 4: matching return value (dbGetReturningExt)
		// 5: full returning array value (dbGetReturningArray)
		return [
			'single insert (PK)' => [
				"INSERT INTO table_with_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, '2022-03-01') "
					. "RETURNING row_varchar, row_varchar_literal, row_int, row_date",
				null,
				null,
				null,
				null,
				[
					'row_varchar' => 'Text',
					'row_varchar_literal' => 'Other',
					'row_int' => 123,
					'row_date' => '2022-03-01',
					// 'table_with_primary_key_id' => "/^\d+$/",
					'table_with_primary_key_id' => $table_with_primary_key_id + 1,
				],
				[
					0 => [
						'row_varchar' => 'Text',
						'row_varchar_literal' => 'Other',
						'row_int' => 123,
						'row_date' => '2022-03-01',
						// 'table_with_primary_key_id' => "/^\d+$/",
						'table_with_primary_key_id' => $table_with_primary_key_id + 1,
					]
				]
			],
			// same but as heredoc
			'single insert (PK), heredoc string' => [
				<<<SQL
				INSERT INTO table_with_primary_key (
					row_varchar, row_varchar_literal, row_int, row_date
				) VALUES (
					'Text', 'Other', 123, '2022-03-01'
				)
				RETURNING row_varchar, row_varchar_literal, row_int, row_date
				SQL,
				null,
				null,
				null,
				null,
				[
					'row_varchar' => 'Text',
					'row_varchar_literal' => 'Other',
					'row_int' => 123,
					'row_date' => '2022-03-01',
					// 'table_with_primary_key_id' => "/^\d+$/",
					'table_with_primary_key_id' => $table_with_primary_key_id + 2,
				],
				[
					0 => [
						'row_varchar' => 'Text',
						'row_varchar_literal' => 'Other',
						'row_int' => 123,
						'row_date' => '2022-03-01',
						// 'table_with_primary_key_id' => "/^\d+$/",
						'table_with_primary_key_id' => $table_with_primary_key_id + 2,
					]
				]
			],
			// double insert (PK)
			'dobule insert (PK)' => [
				"INSERT INTO table_with_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, '2022-03-01'), "
					. "('Foxtrott', 'Tango', 789, '1982-10-15') "
					. "RETURNING row_varchar, row_varchar_literal, row_int, row_date",
				null,
				null,
				null,
				null,
				[
					0 => [
						'row_varchar' => 'Text',
						'row_varchar_literal' => 'Other',
						'row_int' => 123,
						'row_date' => '2022-03-01',
						'table_with_primary_key_id' => $table_with_primary_key_id + 3,
					],
					1 => [
						'row_varchar' => 'Foxtrott',
						'row_varchar_literal' => 'Tango',
						'row_int' => 789,
						'row_date' => '1982-10-15',
						'table_with_primary_key_id' => $table_with_primary_key_id + 4,
					],
				],
				[
					0 => [
						'row_varchar' => 'Text',
						'row_varchar_literal' => 'Other',
						'row_int' => 123,
						'row_date' => '2022-03-01',
						'table_with_primary_key_id' => $table_with_primary_key_id + 3,
					],
					1 => [
						'row_varchar' => 'Foxtrott',
						'row_varchar_literal' => 'Tango',
						'row_int' => 789,
						'row_date' => '1982-10-15',
						'table_with_primary_key_id' => $table_with_primary_key_id + 4,
					],
				]
			],
			// insert into table with no primary key
			'single insert (No PK)' => [
				"INSERT INTO table_without_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, '2022-03-01') "
					. "RETURNING row_varchar, row_varchar_literal, row_int, row_date",
				null,
				null,
				null,
				null,
				[
					'row_varchar' => 'Text',
					'row_varchar_literal' => 'Other',
					'row_int' => 123,
					'row_date' => '2022-03-01',
				],
				[
					0 => [
						'row_varchar' => 'Text',
						'row_varchar_literal' => 'Other',
						'row_int' => 123,
						'row_date' => '2022-03-01',
					]
				]
			],
			// same as above but as heredoc string
			'single insert (No PK), heredoc string' => [
				<<<SQL
				INSERT INTO table_without_primary_key (
					row_varchar, row_varchar_literal, row_int, row_date
				) VALUES (
					'Text', 'Other', 123, '2022-03-01'
				)
				RETURNING row_varchar, row_varchar_literal, row_int, row_date
				SQL,
				null,
				null,
				null,
				null,
				[
					'row_varchar' => 'Text',
					'row_varchar_literal' => 'Other',
					'row_int' => 123,
					'row_date' => '2022-03-01',
				],
				[
					0 => [
						'row_varchar' => 'Text',
						'row_varchar_literal' => 'Other',
						'row_int' => 123,
						'row_date' => '2022-03-01',
					]
				]
			],
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbGetReturningExt
	 * @covers ::dbGetReturningArray
	 * @dataProvider returningPrvoider
	 * @testdox Check returning cursor using $pk_name with $key and $pos [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed<|null $params
	 * @param string|null $pk_name
	 * @param string|null $key
	 * @param integer|null $pos
	 * @param array<mixed>|string|int|null $expected_ret_ext
	 * @param array $expected_ret_arr
	 * @return void
	 */
	public function testDbReturning(
		string $query,
		?array $params,
		?string $pk_name,
		?string $key,
		?int $pos,
		array|string|int|null $expected_ret_ext,
		array $expected_ret_arr
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// insert data
		if ($pk_name === null && $params === null) {
			$db->dbExec($query);
		} elseif ($params === null) {
			$db->dbExec($query, $pk_name);
		} elseif ($pk_name === null) {
			$db->dbExecParams($query, $params);
		} else {
			$db->dbExecParams($query, $params, $pk_name);
		}

		// get the last value for PK and match that somehow

		$returning_ext = $db->dbGetReturningExt($key, $pos);
		$returning_arr = $db->dbGetReturningArray();

		$this->assertEquals(
			$expected_ret_ext,
			$returning_ext,
			'Returning extended failed'
		);
		$this->assertEquals(
			$expected_ret_arr,
			$returning_arr,
			'Returning Array failed'
		);

		// print "EXT: " . print_r($returning_ext, true) . "\n";
		// print "ARR: " . print_r($returning_arr, true) . "\n";

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// testing auto convert

	/**
	 * Undocumented function
	 *
	 * @covers ::dbSetConvertFlag
	 * @testdox Check convert type works
	 *
	 * @return void
	 */
	public function testConvertType(): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$bytea_data = $db->dbEscapeBytea(
			file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'CoreLibsDBIOTest.php') ?: ''
		);
		$query_insert = <<<SQL
		INSERT INTO table_with_primary_key (
			uid,
			row_int, row_numeric, row_varchar, row_varchar_literal,
			row_json, row_jsonb, row_bytea, row_timestamp,
			row_date, row_interval, row_array_int, row_array_varchar
		) VALUES (
			$1,
			$2, $3, $4, $5,
			$6, $7, $8, $9,
			$10, $11, $12, $13
		)
		SQL;
		$db->dbExecParams(
			$query_insert,
			[
				'CONVERT_TYPE_TEST',
				1, 1.5, 'varchar', 'varchar literla',
				json_encode(['json', 'a', 1, true, 'sub' => ['b', 'c']]),
				json_encode(['jsonb', 'a', 1, true, 'sub' => ['b', 'c']]),
				$bytea_data, date('Y-m-d H:i:s'), date('Y-m-d'), date('H:m:s'),
				'{1,2,3}', '{"a","b","c"}'
			]
		);
		$type_layout = [
			'uid' => 'string',
			'row_int' => 'int',
			'row_numeric' => 'float',
			'row_varchar' => 'string',
			'row_varchar_literal' => 'string',
			'row_json' => 'json',
			'row_jsonb' => 'json',
			'row_bytea' => 'bytea',
			'row_timestamp' => 'string',
			'row_date' => 'string',
			'row_interval' => 'string',
			'row_array_int' => 'string',
			'row_array_varchar' => 'string'
		];
		$query_select = <<<SQL
		SELECT
			uid,
			row_int, row_numeric, row_varchar, row_varchar_literal,
			row_json, row_jsonb, row_bytea, row_timestamp,
			row_date, row_interval, row_array_int, row_array_varchar
		FROM
			table_with_primary_key
		WHERE
			uid = $1
		SQL;
		$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
		// all hast to be string
		foreach ($res as $key => $value) {
			$this->assertIsString($value, 'Assert string for column: ' . $key);
		}
		// convert base only
		$db->dbSetConvertFlag(Convert::on);
		$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
		foreach ($res as $key => $value) {
			if (is_numeric($key)) {
				$name = $db->dbGetFieldName($key);
			} else {
				$name = $key;
			}
			switch ($type_layout[$name]) {
				case 'int':
					$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
					break;
				default:
					$this->assertIsString($value, 'Assert string for column: ' . $key  . '/' . $name);
					break;
			}
		}
		$db->dbSetConvertFlag(Convert::numeric);
		$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
		foreach ($res as $key => $value) {
			if (is_numeric($key)) {
				$name = $db->dbGetFieldName($key);
			} else {
				$name = $key;
			}
			switch ($type_layout[$name]) {
				case 'int':
					$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
					break;
				case 'float':
					$this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
					break;
				default:
					$this->assertIsString($value, 'Assert string for column: ' . $key  . '/' . $name);
					break;
			}
		}
		$db->dbSetConvertFlag(Convert::json);
		$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
		foreach ($res as $key => $value) {
			if (is_numeric($key)) {
				$name = $db->dbGetFieldName($key);
			} else {
				$name = $key;
			}
			switch ($type_layout[$name]) {
				case 'int':
					$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
					break;
				case 'float':
					$this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
					break;
				case 'json':
				case 'jsonb':
					$this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name);
					break;
				default:
					$this->assertIsString($value, 'Assert string for column: ' . $key  . '/' . $name);
					break;
			}
		}
		$db->dbSetConvertFlag(Convert::bytea);
		$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
		foreach ($res as $key => $value) {
			if (is_numeric($key)) {
				$name = $db->dbGetFieldName($key);
			} else {
				$name = $key;
			}
			switch ($type_layout[$name]) {
				case 'int':
					$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
					break;
				case 'float':
					$this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
					break;
				case 'json':
				case 'jsonb':
					$this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name);
					break;
				case 'bytea':
					// for hex types it must not start with \x
					$this->assertStringStartsNotWith(
						'\x',
						$value,
						'Assert bytes not starts with \x for column: ' . $key . '/' . $name
					);
					break;
				default:
					$this->assertIsString($value, 'Assert string for column: ' . $key  . '/' . $name);
					break;
			}
		}
	}

	// - internal read data (post exec)
	//   dbGetNumRows, dbGetNumFields, dbGetFieldNames,
	//   dbGetQuery, dbGetQueryHash, dbGetDbh

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function getMethodsProvider(): array
	{
		// 0: run query
		// 1: query params
		// 2: optional insert query (if select or needed)
		// 3: optional compare query, if not set 0 is used
		// 4: num rows
		// 5: column count
		// 6: column names
		// 7: column types
		return [
			'select data' => [
				"SELECT row_varchar, row_varchar_literal, row_int, row_date "
					. "FROM table_with_primary_key",
				null,
				"INSERT INTO table_with_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, '2022-03-01'), "
					. "('Foxtrott', 'Tango', 789, '1982-10-15') ",
				null,
				//
				3,
				4,
				['row_varchar', 'row_varchar_literal', 'row_int', 'row_date'],
				['varchar', 'varchar', 'int4', 'date'],
			],
			// insert
			'insert data' => [
				"INSERT INTO table_with_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_numeric, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, 1.0, '2022-03-01'), "
					. "('Foxtrott', 'Tango', 789, 2.2, '1982-10-15'), "
					. "('Schlamm', 'Beizinger', 100, 3.14, '1990-1-1') ",
				null,
				null,
				"INSERT INTO table_with_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_numeric, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, 1.0, '2022-03-01'), "
					. "('Foxtrott', 'Tango', 789, 2.2, '1982-10-15'), "
					. "('Schlamm', 'Beizinger', 100, 3.14, '1990-1-1') "
					. " RETURNING table_with_primary_key_id",
				//
				3,
				0,
				[],
				[],
			],
			// update
			'update data' => [
				"UPDATE table_with_primary_key SET "
					. "row_varchar = 'CHANGE A', row_int = 999 "
					. "WHERE uid = 'A'",
					null,
				"INSERT INTO table_with_primary_key "
					. "(uid, row_varchar, row_varchar_literal, row_int, row_date) "
					. "VALUES "
					. "('A', 'Text', 'Other', 123, '2022-03-01'), "
					. "('B', 'Foxtrott', 'Tango', 789, '1982-10-15') ",
				null,
				//
				1,
				0,
				[],
				[],
			],
			// select with params and proper query hashing
			'select data, params' => [
				"SELECT row_varchar, row_varchar_literal, row_int, row_date "
					. "FROM table_with_primary_key "
					. "WHERE row_varchar = $1",
				['Text'],
				"INSERT INTO table_with_primary_key "
					. "(row_varchar, row_varchar_literal, row_int, row_date) "
					. "VALUES "
					. "('Text', 'Other', 123, '2022-03-01'), "
					. "('Foxtrott', 'Tango', 789, '1982-10-15') ",
				null,
				//
				1,
				4,
				['row_varchar', 'row_varchar_literal', 'row_int', 'row_date'],
				['varchar', 'varchar', 'int4', 'date'],
			],
			// something other (schema change?)
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbGetNumRows
	 * @covers ::dbGetNumFields
	 * @covers ::dbGetFieldNames
	 * @covers ::dbGetQuery
	 * @covers ::dbGetQueryHash
	 * @covers ::dbGetDbh
	 * @dataProvider getMethodsProvider
	 * @testdox Check check rows: $expected_rows and cols: $expected_cols [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed>|null $params,
	 * @param string|null $insert_query
	 * @param string|null $compare_query
	 * @param integer $expected_rows
	 * @param integer $expected_cols
	 * @param array<mixed> $expected_col_names
	 * @param array<mixed> $expected_col_types
	 * @return void
	 */
	public function testDbGetMethods(
		string $query,
		?array $params,
		?string $insert_query,
		?string $compare_query,
		int $expected_rows,
		int $expected_cols,
		array $expected_col_names,
		array $expected_col_types
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		if (!empty($insert_query)) {
			$db->dbExec($insert_query);
		}

		if ($params === null) {
			$db->dbExec($query);
		} else {
			$db->dbExecParams($query, $params);
		}

		$this->assertInstanceOf(
			'PgSql\Result',
			$db->dbGetCursor(),
			'Failed assert dbGetCursor'
		);

		$this->assertEquals(
			$compare_query ?? $query,
			$db->dbGetQuery(),
			'Failed assert dbGetQuery'
		);
		$this->assertEquals(
			// perhaps move that somewhere else?
			\CoreLibs\Create\Hash::__hashLong(
				$query . (
					$params !== null && $params !== [] ?
						'#' . json_encode($params) : ''
				)
			),
			($params === null ?
				$db->dbBuildQueryHash($query) :
				$db->dbBuildQueryHash($query, $params)
			),
			'Failed assertdbGetQueryHash '
		);
		$this->assertEquals(
			$expected_rows,
			$db->dbGetNumRows(),
			'Failed assert dbGetNumRows'
		);
		$this->assertEquals(
			$expected_cols,
			$db->dbGetNumFields(),
			'Failed assert dbGetNumFields'
		);
		$this->assertEquals(
			$expected_col_names,
			$db->dbGetFieldNames(),
			'Failed assert dbGetFieldNames'
		);
		$this->assertEquals(
			$expected_col_types,
			$db->dbGetFieldTypes(),
			'Failed assert dbGetFieldTypes'
		);
		// check FieldNameTypes matches
		$this->assertEquals(
			array_combine(
				$expected_col_names,
				$expected_col_types
			),
			$db->dbGetFieldNameTypes(),
			'Failed assert dbGetFieldNameTypes'
		);
		// check pos matches name
		// name matches type
		// pos matches type
		foreach ($expected_col_names as $pos => $name) {
			$this->assertEquals(
				$name,
				$db->dbGetFieldName($pos),
				'Failed assert dbGetFieldName: ' . $pos . ' => ' . $name
			);
			$this->assertEquals(
				$expected_col_types[$pos],
				$db->dbGetFieldType($name),
				'Failed assert dbGetFieldType: ' . $name . ' => ' . $expected_col_types[$pos]
			);
			$this->assertEquals(
				$expected_col_types[$pos],
				$db->dbGetFieldType($pos),
				'Failed assert dbGetFieldType: ' . $pos . ' => ' . $expected_col_types[$pos]
			);
		}
		$dbh = $db->dbGetDbh();
		$this->assertIsObject(
			$dbh
		);
		// also check that this is correct instance type
		$this->assertInstanceOf(
			'PgSql\Connection',
			$dbh
		);

		// if this is a select query, db dbReturn, dbReturnRow, dbReturnArray too
		if (preg_match("/^(select|show|with) /i", $query)) {
			// dbReturn
			$db->dbReturn($query);
			$this->assertEquals(
				$expected_rows,
				$db->dbGetNumRows()
			);
			$this->assertEquals(
				$expected_cols,
				$db->dbGetNumFields()
			);
			$this->assertEquals(
				$expected_col_names,
				$db->dbGetFieldNames()
			);
			// dbReturnRow
			// will return ALL rows there, but returns only the first
			$db->dbReturnRow($query);
			$this->assertEquals(
				$expected_rows,
				$db->dbGetNumRows()
			);
			$this->assertEquals(
				$expected_cols,
				$db->dbGetNumFields()
			);
			$this->assertEquals(
				$expected_col_names,
				$db->dbGetFieldNames()
			);
			// dbReturnArray
			$db->dbReturnArray($query);
			$this->assertEquals(
				$expected_rows,
				$db->dbGetNumRows()
			);
			$this->assertEquals(
				$expected_cols,
				$db->dbGetNumFields()
			);
			$this->assertEquals(
				$expected_col_names,
				$db->dbGetFieldNames()
			);
		}

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}

	// MARK: QUERY PLACEHOLDERS

	// test query placeholder detection for all possible sets
	// ::dbPrepare

	/**
	 * placeholder sql
	 *
	 * @return array
	 */
	public function providerDbCountQueryParams(): array
	{
		return [
			'one place holder' => [
				'query' => 'SELECT row_varchar FROM table_with_primary_key WHERE row_varchar = $1',
				'count' => 1,
				'convert' => false,
			],
			'one place holder, json call' => [
				'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_jsonb->>'data' = $1",
				'count' => 1,
				'convert' => false,
			],
			'one place holder, <> compare' => [
				'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar <> $1",
				'count' => 1,
				'convert' => false,
			],
			'one place holder, named' => [
				'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar <> :row_varchar",
				'count' => 1,
				'convert' => true,
			],
			'no replacement' => [
				'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar = '$1'",
				'count' => 0,
				'convert' => false,
			],
			'insert' => [
				'query' => "INSERT INTO table_with_primary_key (row_varchar, row_jsonb, row_int) VALUES ($1, $2, $3)",
				'count' => 3,
				'convert' => false,
			],
			'update' => [
				'query' => "UPDATE table_with_primary_key SET row_varchar = $1, row_jsonb = $2, row_int = $3 WHERE row_numeric = $4",
				'count' => 4,
				'convert' => false,
			],
			'multiple, multline' => [
				'query' => <<<SQL
				SELECT
					row_varchar
				FROM
					table_with_primary_key
				WHERE
					row_varchar = $1 AND row_int = $2
					AND row_numeric = ANY($3)
				SQL,
				'count' => 3,
				'convert' => false,
			],
			'two digit numbers' => [
				'query' => <<<SQL
				INSERT INTO table_with_primary_key (
					row_int, row_numeric, row_varchar, row_varchar_literal, row_json,
					row_jsonb, row_bytea, row_timestamp, row_date, row_interval
				) VALUES (
					$1, $2, $3, $4, $5,
					$6, $7, $8, $9, $10
				)
				SQL,
				'count' => 10,
				'convert' => false,
			],
			'things in brackets' => [
				'query' => <<<SQL
				SELECT row_varchar
				FROM table_with_primary_key
				WHERE
					row_varchar = $1 AND
					(row_int = ANY($2) OR row_int = $3)
					AND row_varchar_literal = $4
				SQL,
				'count' => 4,
				'convert' => false,
			],
			'number compare' => [
				'query' => <<<SQL
				SELECT row_varchar
				FROM table_with_primary_key
				WHERE
					row_int >= $1 OR row_int <= $2 OR
					row_int > $3 OR row_int < $4
					OR row_int = $5 OR row_int <> $6
				SQL,
				'count' => 6,
				'convert' => false,
			],
			'comments in insert' => [
				'query' => <<<SQL
				INSERT INTO table_with_primary_key (
					row_int, row_numeric, row_varchar, row_varchar_literal
				) VALUES (
					-- comment 1 かな
					$1, $2,
					-- comment 2 -
					$3
					-- comment 3
					, $4
					-- ignore $5, $6
					-- $7, $8
					-- digest($9, 10)
				)
				SQL,
				'count' => 4,
				'convert' => false
			],
			'comment in update' => [
				'query' => <<<SQL
				UPDATE table_with_primary_key SET
					row_int =
					-- COMMENT 1
					$1,
					row_numeric =
					$2 -- COMMENT 2
					,
					row_varchar -- COMMENT 3
					= $3
				WHERE
					row_varchar = $4
				SQL,
				'count' => 4,
				'convert' => false,
			],
			// Note some are not set
			'a complete set of possible' => [
				'query' => <<<SQL
				UPDATE table_with_primary_key SET
				-- ROW
				row_varchar = $1
				WHERE
				row_varchar = ANY($2) AND row_varchar <> $3
				AND row_varchar > $4 AND row_varchar < $5
				AND row_varchar >= $6 AND row_varchar <=$7
				AND row_jsonb->'a' = $8 AND row_jsonb->>$9 = 'a'
				AND row_jsonb<@$10 AND row_jsonb@>$11
				AND row_varchar ^@ $12
				SQL,
				'count' => 12,
				'convert' => false,
			],
			// all the same
			'all the same numbered' => [
				'query' => <<<SQL
				UPDATE table_with_primary_key SET
					row_int = $1::INT, row_numeric = $1::NUMERIC, row_varchar = $1
				WHERE
					row_varchar = $1
				SQL,
				'count' => 1,
				'convert' => false,
			],
			'update with case' => [
				'query' => <<<SQL
				UPDATE table_with_primary_key SET
					row_int = $1::INT,
					row_varchar = CASE WHEN row_int = 1 THEN $2 ELSE 'bar'::VARCHAR END
				WHERE
					row_varchar = $3
				SQL,
				'count' => 3,
				'convert' => false,
			],
			'select with case' => [
				'query' => <<<SQL
				SELECT row_int
				FROM table_with_primary_key
				WHERE
					row_varchar = CASE WHEN row_int = 1 THEN $1 ELSE $2 END
				SQL,
				'count' => 2,
				'convert' => false,
			],
			// special $$ string case
			'text string, with $ placehoders that could be seen as $$ string' => [
				'query' => <<<SQL
				SELECT row_int
				FROM table_with_primary_key
				WHERE
					row_bytea = digest($3::VARCHAR, $4) OR
					row_varchar = encode(digest($3, $4), 'hex') OR
					row_bytea = hmac($3, $5, $4) OR
					row_varchar = encode(hmac($3, $5, $4), 'hex') OR
					row_bytea = pgp_sym_encrypt($3, $6) OR
					row_varchar = encode(pgp_sym_encrypt($1, $6), 'hex') OR
					row_varchar = CASE WHEN row_int = 1 THEN $1 ELSE $2 END
				SQL,
				'count' => 6,
				'convert' => false,
			],
			// NOTE, in SQL heredoc we cannot write $$ strings parts
			'text string, with $ placehoders are in $$ strings' => [
				'query' => '
				SELECT row_int
				FROM table_with_primary_key
				WHERE
					row_varchar = $$some string$$ OR
					row_varchar = $tag$some string$tag$ OR
					row_varchar = $btag$some $1 string$btag$ OR
					row_varchar = $btag$some $1 $subtag$ something $subtag$string$btag$ OR
					row_varchar = $1
				',
				'count' => 1,
				'convert' => false,
			],
			// a text string with escaped quite
			'text string, with escaped quote' => [
				'query' => <<<SQL
				SELECT row_int
				FROM table_with_primary_key
				WHERE
					row_varchar = 'foo bar bar baz $5' OR
					row_varchar = 'foo bar '' barbar $6' OR
					row_varchar = E'foo bar \' barbar $7' OR
					row_varchar = CASE WHEN row_int = 1 THEN $1 ELSE $2 END
				SQL,
				'count' => 2,
				'convert' => false,
			]
		];
		$string = <<<SQL
		'''
		SQL;
	}

	/**
	 * Placeholder check and convert tests
	 *
	 * @covers ::dbPrepare
	 * @covers ::__dbCountQueryParams
	 * @onvers ::convertPlaceholderInQuery
	 * @dataProvider providerDbCountQueryParams
	 * @testdox Query replacement count test [$_dataName]
	 *
	 * @param  string $query
	 * @param  int    $count
	 * @return void
	 */
	public function testDbCountQueryParams(string $query, int $count, bool $convert): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$id = sha1($query);
		$db->dbSetConvertPlaceholder($convert);
		$db->dbPrepare($id, $query);
		// print "\n**\n";
		// print "PCount: " . $db->dbGetPrepareCursorValue($id, 'count') . "\n";
		// print "\n**\n";
		$this->assertEquals(
			$count,
			$db->dbGetPrepareCursorValue($id, 'count'),
			'DB count params'
		);
		$placeholder = ConvertPlaceholder::convertPlaceholderInQuery($query, null, 'pg');
		// print "RES: " . print_r($placeholder, true) . "\n";
		$this->assertEquals(
			$count,
			$placeholder['needed'],
			'convert params'
		);
	}

	/**
	 * query placeholder convert
	 *
	 * @return array
	 */
	public function queryPlaceholderReplaceProvider(): array
	{
		// 				WHERE row_varchar = $1
		return [
			'select, no change' => [
				'query' => <<<SQL
				SELECT row_varchar, row_varchar_literal, row_int, row_date
				FROM table_with_primary_key
				SQL,
				'params' => [],
				'found' => 0,
				'expected_query' => '',
				'expected_params' => [],
			],
			'select, params ?' => [
				'query' => <<<SQL
				SELECT row_varchar, row_varchar_literal, row_int, row_date
				FROM table_with_primary_key
				WHERE row_varchar = ?
				SQL,
				'params' => ['string a'],
				'found' => 1,
				'expected_query' => <<<SQL
				SELECT row_varchar, row_varchar_literal, row_int, row_date
				FROM table_with_primary_key
				WHERE row_varchar = $1
				SQL,
				'expected_params' => ['string a'],
			],
			'select, params :' => [
				'query' => <<<SQL
				SELECT row_varchar, row_varchar_literal, row_int, row_date
				FROM table_with_primary_key
				WHERE row_varchar = :row_varchar
				SQL,
				'params' => [':row_varchar' => 'string a'],
				'found' => 1,
				'expected_query' => <<<SQL
				SELECT row_varchar, row_varchar_literal, row_int, row_date
				FROM table_with_primary_key
				WHERE row_varchar = $1
				SQL,
				'expected_params' => ['string a'],
			],
			// TODO: test with multiple entries
			// TODO: test with same entry ($1, $1, :var, :var)
		];
	}

	/**
	 * test query string with placeholders convert
	 *
	 * @dataProvider queryPlaceholderReplaceProvider
	 * @testdox Query replacement test [$_dataName]
	 *
	 * @param  string $query
	 * @param  array  $params
	 * @param  string $expected_query
	 * @param  array  $expected_params
	 * @return void
	 */
	public function testQueryPlaceholderReplace(
		string $query,
		array $params,
		int $expected_found,
		string $expected_query,
		array $expected_params
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$db->dbSetConvertPlaceholder(true);
		//
		if ($db->dbCheckQueryForSelect($query)) {
			$res = $db->dbReturnRowParams($query, $params);
			$converted = $db->dbGetPlaceholderConverted();
		} else {
			$db->dbExecParams($query, $params);
			$converted = $db->dbGetPlaceholderConverted();
		}
		$this->assertEquals(
			$expected_found,
			$converted['found'],
			'Found not equal'
		);
		$this->assertEquals(
			$expected_query,
			$converted['query'],
			'Query not equal'
		);
		$this->assertEquals(
			$expected_params,
			$converted['params'],
			'Params not equal'
		);
	}

	/**
	 * test exception for placeholder convert
	 * -> internally converted to error
	 *
	 * @testdox Query Replace error tests
	 *
	 * @return void
	 */
	public function testQueryPlaceholderReplaceException(): void
	{
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);
		$db->dbSetConvertPlaceholder(true);
		$db->dbExecParams(
			<<<SQL
			SELECT foo FROM bar
			WHERE a = ? and b = :bname
			SQL,
			['a', 'b']
		);
		$this->assertEquals(
			200,
			$db->dbGetLastError()
		);

		// catch unset, for :names
		$db->dbExecParams(
			<<<SQL
			SELECT foo FROM bar
			WHERE a = :aname and b = :bname
			SQL,
			[':foo' => 'a', ':bname' => 'b']
		);
		$this->assertEquals(
			210,
			$db->dbGetLastError()
		);

		// TODO: other way around for to pdo
	}

	// TODO implement below checks
	// - complex write sets
	//   dbWriteData, dbWriteDataExt
	// - data debug
	//   dbDumpData

	// MARK: ASYNC

	// ASYNC at the end because it has 1s timeout
	// - asynchronous executions
	//   dbExecAsync, dbCheckAsync

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function asyncProvider(): array
	{
		// 0: query
		// 1: params
		// 2: primary key
		// 3: exepected exec return
		// 4: warning
		// 5: error
		// 6: exepcted check return 1st
		// 7: final result
		// 8: warning
		// 9: error
		return [
			'run simple async query' => [
				"SELECT pg_sleep(1)",
				null,
				null,
				// exec result
				true,
				'',
				'',
				// check first
				true,
				// check final
				'result',
				'',
				''
			],
			'run simple async query, params' => [
				"SELECT pg_sleep($1)",
				[1],
				null,
				// exec result
				true,
				'',
				'',
				// check first
				true,
				// check final
				'result',
				'',
				''
			],
			// error on wrong params
			'wrong params count' => [
				"SELECT pg_sleep($1)",
				[],
				null,
				// exec result
				false,
				'',
				'23',
				// check first
				false,
				// check final
				false,
				'',
				'42'
			],
			// send query failed (E40)
			// result failed (E43)
			// no query running (E42)
			'no async query running' => [
				'',
				null,
				null,
				//
				false,
				'',
				'11',
				//
				false,
				//
				false,
				'',
				'42'
			]
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::dbExecAsync
	 * @covers ::dbExecParamsAsync
	 * @covers ::dbCheckAsync
	 * @dataProvider asyncProvider
	 * @testdox async query $query with $expected_exec (warning $warning_exec/error $error_exec) and $expected_check/$expected_final (warning $warning_final/error $error_final) [$_dataName]
	 *
	 * @param string $query
	 * @param array<mixed>|null $params
	 * @param string|null $pk_name
	 * @param boolean $expected_exec
	 * @param string $warning_exec
	 * @param string $error_exec
	 * @param bool $expected_check
	 * @param bool|object|resource $expected_final
	 * @param string $warning_final
	 * @param string $error_final
	 * @return void
	 */
	public function testDbExecAsync(
		string $query,
		?array $params,
		?string $pk_name,
		bool $expected_exec,
		string $warning_exec,
		string $error_exec,
		bool $expected_check,
		$expected_final,
		string $warning_final,
		string $error_final
	): void {
		$db = new \CoreLibs\DB\IO(
			self::$db_config['valid'],
			self::$log
		);

		// exec the query
		if ($pk_name === null && $params === null) {
			$result_exec = $db->dbExecAsync($query);
		} elseif ($params === null) {
			$result_exec = $db->dbExecAsync($query, $pk_name);
		} elseif ($pk_name === null) {
			$result_exec = $db->dbExecParamsAsync($query, $params);
		} else {
			$result_exec = $db->dbExecParamsAsync($query, $params, $pk_name);
		}
		$this->assertEquals(
			$expected_exec,
			$result_exec
		);
		// error/warning check
		$this->subAssertErrorTest($db, $warning_exec, $error_exec);

		$run = 1;
		// first loop check
		while (($result_check = $db->dbCheckAsync()) === true) {
			if ($run == 1) {
				$this->assertEquals(
					$expected_check,
					$result_check
				);
			}
			$run++;
		}
		// check after final
		if ($expected_final == 'result') {
			// post end check
			$this->assertIsObject(
				$result_check
			);
			// also check that this is correct instance type
			$this->assertInstanceOf(
				'PgSql\Result',
				$result_check
			);
		} else {
			// else compar check
			$this->assertEquals(
				$expected_final,
				$result_check
			);
		}
		// error/warning check
		$this->subAssertErrorTest($db, $warning_final, $error_final);

		// reset all data
		$db->dbExec("TRUNCATE table_with_primary_key");
		$db->dbExec("TRUNCATE table_without_primary_key");
		// close connection
		$db->dbClose();
	}
}

// __END__
