diff --git a/4dev/tests/DB/CoreLibsDBIOTest.php b/4dev/tests/DB/CoreLibsDBIOTest.php index d1d598f3..93703db8 100644 --- a/4dev/tests/DB/CoreLibsDBIOTest.php +++ b/4dev/tests/DB/CoreLibsDBIOTest.php @@ -232,7 +232,7 @@ final class CoreLibsDBIOTest extends TestCase $this->assertEquals( $error, $last_error, - 'Assert query warning' + 'Assert query error' ); return [$last_warning, $last_error]; } @@ -251,8 +251,6 @@ final class CoreLibsDBIOTest extends TestCase */ public function testDbVersion(): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -276,8 +274,6 @@ final class CoreLibsDBIOTest extends TestCase */ public function testDbVersionNumeric(): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -306,8 +302,6 @@ final class CoreLibsDBIOTest extends TestCase */ public function testDbVersionInfoParameters(): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -365,8 +359,6 @@ final class CoreLibsDBIOTest extends TestCase */ public function testDbVersionInfo(string $parameter, string $expected): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -1592,8 +1584,6 @@ final class CoreLibsDBIOTest extends TestCase string $error, bool $run_many_times = false ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -1832,8 +1822,6 @@ final class CoreLibsDBIOTest extends TestCase string $error, string $insert_data ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -2002,8 +1990,6 @@ final class CoreLibsDBIOTest extends TestCase string $error, string $insert_data ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -3069,8 +3055,6 @@ final class CoreLibsDBIOTest extends TestCase string $error, string $insert_data ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -3465,7 +3449,7 @@ final class CoreLibsDBIOTest extends TestCase $read_query, null, null, - // + // warning: 20 true, '20', '', // 'result', '', '', @@ -3482,6 +3466,31 @@ final class CoreLibsDBIOTest extends TestCase 'returning_id' => false, ], ], + // 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, + ], + ], // insert wrong data count compared to needed (execute 23) 'wrong parmeter count' => [ 'wrong_param_count', @@ -3554,8 +3563,6 @@ final class CoreLibsDBIOTest extends TestCase string $insert_data, array $prepare_cursor, ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -3575,6 +3582,9 @@ final class CoreLibsDBIOTest extends TestCase $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 @@ -3597,66 +3607,68 @@ final class CoreLibsDBIOTest extends TestCase // for non fail prepare test exec // check test result - $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; + 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 + ); } - $this->assertEquals( - $expected_data, - $compare_data - ); - } - // check dbGetPrepareCursorValue - foreach (['pk_name', 'count', 'query', 'returning_id'] as $key) { - $this->assertEquals( - $prepare_cursor[$key], - $db->dbGetPrepareCursorValue($stm_name, $key), - 'Prepared cursor: ' . $key . ': failed assertion' - ); + // check dbGetPrepareCursorValue + foreach (['pk_name', 'count', 'query', 'returning_id'] as $key) { + $this->assertEquals( + $prepare_cursor[$key], + $db->dbGetPrepareCursorValue($stm_name, $key), + 'Prepared cursor: ' . $key . ': failed assertion' + ); + } } // reset all data @@ -3844,8 +3856,6 @@ final class CoreLibsDBIOTest extends TestCase string $expected_get_var, string $expected_get_db ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config[$connection], self::$log @@ -3910,7 +3920,10 @@ final class CoreLibsDBIOTest extends TestCase // 'main::run::run::run::run::run::run::run::runBare::runTest::testDbErrorHandling::dbSetMaxQueryCall 'source' => "/^(include::)?main::(run::)+runBare::runTest::testDbErrorHandling::dbSetMaxQueryCall$/", 'pg_error' => '', - 'msg' => '', + 'message' => '', + 'context' => [ + 'max_calls' => 0 + ] ] ], 'trigger warning' => [ @@ -3943,8 +3956,6 @@ final class CoreLibsDBIOTest extends TestCase string $error_id, array $expected_history ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -3970,7 +3981,7 @@ final class CoreLibsDBIOTest extends TestCase foreach ($expected_history as $key => $value) { // check if starts with / because this is regex (timestamp) // if (substr($expected_2, 0, 1) == '/) { - if (strpos($value, '/') === 0) { + if (!is_array($value) && strpos($value, '/') === 0) { // this is regex $this->assertMatchesRegularExpression( $value, @@ -4058,8 +4069,6 @@ final class CoreLibsDBIOTest extends TestCase bool $expected_set_flag, string $expected_get_encoding ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config[$connection], self::$log @@ -4141,8 +4150,6 @@ final class CoreLibsDBIOTest extends TestCase ?string $encoding_php, string $text ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config[$connection], self::$log @@ -4272,8 +4279,6 @@ final class CoreLibsDBIOTest extends TestCase string $table, string $primary_key ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -4330,7 +4335,7 @@ final class CoreLibsDBIOTest extends TestCase // 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 = 68; + $table_with_primary_key_id = 70; // 0: query + returning // 1: params // 1: pk name for db exec @@ -4530,8 +4535,6 @@ final class CoreLibsDBIOTest extends TestCase array|string|int|null $expected_ret_ext, array $expected_ret_arr ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -4875,8 +4878,6 @@ final class CoreLibsDBIOTest extends TestCase array $expected_col_names, array $expected_col_types ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log @@ -5030,6 +5031,147 @@ final class CoreLibsDBIOTest extends TestCase $db->dbClose(); } + // query placeholder convert + + public function queryPlaceholderReplaceProvider(): array + { + // WHERE row_varchar = $1 + return [ + 'select, no change' => [ + 'query' => << [], + 'found' => 0, + 'expected_query' => '', + 'expected_params' => [], + ], + 'select, params ?' => [ + 'query' => << ['string a'], + 'found' => 1, + 'expected_query' => << ['string a'], + ], + 'select, params :' => [ + 'query' => << [':row_varchar' => 'string a'], + 'found' => 1, + 'expected_query' => << ['string a'], + ] + ]; + } + + /** + * 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( + <<assertEquals( + 200, + $db->dbGetLastError() + ); + + // catch unset, for :names + $db->dbExecParams( + << 'a', ':bname' => 'b'] + ); + $this->assertEquals( + 210, + $db->dbGetLastError() + ); + + // TODO: other way around for to pdo + } + // TODO implement below checks // - complex write sets // dbWriteData, dbWriteDataExt @@ -5158,8 +5300,6 @@ final class CoreLibsDBIOTest extends TestCase string $warning_final, string $error_final ): void { - // self::$log->setLogLevelAll('debug', true); - // self::$log->setLogLevelAll('print', true); $db = new \CoreLibs\DB\IO( self::$db_config['valid'], self::$log diff --git a/www/admin/class_test.db.php b/www/admin/class_test.db.php index c791d8a9..848d0f13 100644 --- a/www/admin/class_test.db.php +++ b/www/admin/class_test.db.php @@ -38,9 +38,10 @@ print ""; print "" . $PAGE_NAME . ""; print ""; print ''; -print ''; +print ''; +print ''; print ''; -print ''; +print ''; print '

' . $PAGE_NAME . '

'; print "LOGFILE NAME: " . $db->log->getLogFile() . "
"; @@ -549,11 +550,13 @@ print ""; print "PREPARE QUERIES
"; // READ PREPARE -$q_prep = "SELECT test_foo_id, test, some_bool, string_a, number_a, " - . "number_a_numeric, some_time " - . "FROM test_foo " - . "WHERE test = $1 " - . "ORDER BY test_foo_id DESC LIMIT 5"; +$q_prep = <<dbPrepare('sel_test_foo', $q_prep) === false) { print "Error in sel_test_foo prepare
"; } else { @@ -681,16 +684,27 @@ echo "
"; $db_pgb = new CoreLibs\DB\IO($DB_CONFIG['test_pgbouncer'] ?? [], $log); print "[PGB] DBINFO: " . $db_pgb->dbInfo() . "
"; if ($db->dbPrepare('pgb_sel_test_foo', $q_prep) === false) { - print "[PGB] [1] Error in pgb_sel_test_foo prepare
"; + print "[PGB] [1] Warning in pgb_sel_test_foo prepare
"; } else { print "[PGB] [1] pgb_sel_test_foo prepare OK
"; } // second prepare if ($db->dbPrepare('pgb_sel_test_foo', $q_prep) === false) { - print "[PGB] [2] Error in pgb_sel_test_foo prepare
"; + print "[PGB] [2] Warning in pgb_sel_test_foo prepare
"; } else { print "[PGB] [2] pgb_sel_test_foo prepare OK
"; } +// same statment name, different query +if ( + $db->dbPrepare('pgb_sel_test_foo', <<"; +} else { + print "[PGB] [3] pgb_sel_test_foo prepare OK
"; +} $db_pgb->dbClose(); # db write class test diff --git a/www/admin/class_test.db.query-placeholder.php b/www/admin/class_test.db.query-placeholder.php new file mode 100644 index 00000000..6da93482 --- /dev/null +++ b/www/admin/class_test.db.query-placeholder.php @@ -0,0 +1,208 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); +// db connection and attach logger +$db = new CoreLibs\DB\IO(DB_CONFIG, $log); +$db->log->debug('START', '=============================>'); + +$PAGE_NAME = 'TEST CLASS: DB QUERY PLACEHOLDER'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print ''; +print '

' . $PAGE_NAME . '

'; + +print "LOGFILE NAME: " . $db->log->getLogFile() . "
"; +print "LOGFILE ID: " . $db->log->getLogFileId() . "
"; +print "DBINFO: " . $db->dbInfo() . "
"; +// DB client encoding +print "DB client encoding: " . $db->dbGetEncoding() . "
"; +print "DB search path: " . $db->dbGetSchema() . "
"; + +$to_db_version = '15.2'; +print "VERSION DB: " . $db->dbVersion() . "
"; +print "SERVER ENCODING: " . $db->dbVersionInfo('server_encoding') . "
"; +if (($dbh = $db->dbGetDbh()) instanceof \PgSql\Connection) { + print "ALL OUTPUT [TEST]:
" . print_r(pg_version($dbh), true) . "

"; +} else { + print "NO DB HANDLER
"; +} +// turn on debug replace for placeholders +$db->dbSetDebugReplacePlaceholder(true); + +print "TRUNCATE test_foo
"; +$db->dbExec("TRUNCATE test_foo"); + +$uniqid = \CoreLibs\Create\Uids::uniqIdShort(); +$binary_data = $db->dbEscapeBytea(file_get_contents('class_test.db.php') ?: ''); +$query_params = [ + $uniqid, + true, + 'STRING A', + 2, + 2.5, + 1, + date('H:m:s'), + date('Y-m-d H:i:s'), + json_encode(['a' => 'string', 'b' => 1, 'c' => 1.5, 'f' => true, 'g' => ['a', 1, 1.5]]), + null, + '{"a", "b"}', + '{1,2}', + '{"(array Text A, 5, 8.8)","(array Text B, 10, 15.2)"}', + '("Text", 4, 6.3)', + $binary_data +]; + +$query_insert = <<dbExecParams($query_insert, $query_params); +echo "*
"; +echo "INSERT ALL COLUMN TYPES: " + . Support::printToString($query_params) . " |
" + . "QUERY: " . $db->dbGetQuery() . " |
" + . "PRIMARY KEY: " . Support::printToString($db->dbGetInsertPK()) . " |
" + . "RETURNING EXT:
" . print_r($db->dbGetReturningExt(), true) . "
|
" + . "RETURNING RETURN:
" . print_r($db->dbGetReturningArray(), true) . "
 |
" + . "ERROR: " . $db->dbGetLastError(true) . "
"; +echo "
"; + +// convert placeholder tests +// ? -> $n +// :name -> $n + +// other way around (just visual) +$test_queries = [ + 'skip' => [ + 'query' => << [], + 'direction' => 'pg', + ], + 'a?' => [ + 'query' => << [\CoreLibs\Create\Uids::uniqIdShort(), 'string A-1', 1234], + 'direction' => 'pg', + ], + 'b:' => [ + 'query' => << [ + ':test' => \CoreLibs\Create\Uids::uniqIdShort(), + ':string_a' => 'string B-1', + ':number_a' => 5678 + ], + 'direction' => 'pg', + ], +]; + +$db->dbSetConvertPlaceholder(true); +foreach ($test_queries as $info => $data) { + $query = $data['query']; + $params = $data['params']; + $direction = $data['direction']; + // print "[$info] Convert: " + // . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params, $direction)) + // . "
"; + if ($db->dbCheckQueryForSelect($query)) { + $row = $db->dbReturnRowParams($query, $params); + print "[$info] SELECT: " . Support::prAr($row) . "
"; + } else { + $db->dbExecParams($query, $params); + } + print "[$info] " . Support::printAr($db->dbGetPlaceholderConverted()) . "
"; + echo "
"; +} + +echo "dbReturn read:
"; +while ( + is_array($res = $db->dbReturnParams( + <<"; +} + +print "CursorExt: " . Support::prAr($db->dbGetCursorExt(<<dbReturnRowParams(<<dbGetPlaceholderConverted()) . "
"; + +print ""; +$db->log->debug('DEBUGEND', '==================================== [END]'); + +// __END__ diff --git a/www/admin/class_test.db.single.php b/www/admin/class_test.db.single.php index 256dc902..ea196ff0 100644 --- a/www/admin/class_test.db.single.php +++ b/www/admin/class_test.db.single.php @@ -16,7 +16,7 @@ define('USE_DATABASE', true); // sample config require 'config.php'; // define log file id -$LOG_FILE_ID = 'classTest-db-single'; +$LOG_FILE_ID = 'classTest-db-query-placeholders'; ob_end_flush(); use CoreLibs\Debug\Support; @@ -30,7 +30,7 @@ $log = new CoreLibs\Logging\Logging([ $db = new CoreLibs\DB\IO(DB_CONFIG, $log); $db->log->debug('START', '=============================>'); -$PAGE_NAME = 'TEST CLASS: DB SINGLE'; +$PAGE_NAME = 'TEST CLASS: DB QUERY PLACEHOLDERS'; print ""; print "" . $PAGE_NAME . ""; print ""; @@ -65,63 +65,6 @@ function testDBS(\CoreLibs\DB\IO $dbc): void $dbc->dbReturnRow("SELECT test FROM test_foo LIMIT 1"); } -$uniqid = \CoreLibs\Create\Uids::uniqIdShort(); -$binary_data = $db->dbEscapeBytea(file_get_contents('class_test.db.php') ?: ''); -$query_params = [ - $uniqid, - true, - 'STRING A', - 2, - 2.5, - 1, - date('H:m:s'), - date('Y-m-d H:m:s'), - json_encode(['a' => 'string', 'b' => 1, 'c' => 1.5, 'f' => true, 'g' => ['a', 1, 1.5]]), - null, - '{"a", "b"}', - '{1,2}', - '{"(array Text A, 5, 8.8)","(array Text B, 10, 15.2)"}', - '("Text", 4, 6.3)', - $binary_data -]; - -$query_insert = <<dbExecParams($query_insert, $query_params); -$query_select = <<dbReturnRowParams($query_select, [$uniqid]); -if (is_array($res)) { - var_dump($res); -} - testDBS($db); print ""; diff --git a/www/admin/class_test.php b/www/admin/class_test.php index c8f528f3..75c9e516 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -69,9 +69,10 @@ print ""; // key: file name, value; name $test_files = [ 'class_test.db.php' => 'Class Test: DB', - 'class_test.db.types.php' => 'Class Test: DB COLUMN TYPES', - 'class_test.db.single.php' => 'Class Test: DB SINGLE', + 'class_test.db.types.php' => 'Class Test: DB column type convert', + 'class_test.db.query-placeholder.php' => 'Class Test: DB query placeholder convert', 'class_test.db.dbReturn.php' => 'Class Test: DB dbReturn', + 'class_test.db.single.php' => 'Class Test: DB single query tests', 'class_test.convert.colors.php' => 'Class Test: CONVERT COLORS', 'class_test.check.colors.php' => 'Class Test: CHECK COLORS', 'class_test.mime.php' => 'Class Test: MIME', diff --git a/www/lib/CoreLibs/DB/IO.php b/www/lib/CoreLibs/DB/IO.php index a4f83395..23014b8b 100644 --- a/www/lib/CoreLibs/DB/IO.php +++ b/www/lib/CoreLibs/DB/IO.php @@ -284,8 +284,8 @@ class IO public const ERROR_HASH_TYPE = 'adler32'; /** @var string regex to get returning with matches at position 1 */ public const REGEX_RETURNING = '/\s+returning\s+(.+\s*(?:.+\s*)+);?$/i'; - /** @var array allowed convert target for placeholder */ - public const DB_CONVERT_PLACEHOLDER_TARGET = ['pg', 'pdo']; + /** @var array allowed convert target for placeholder: pg or pdo (currently not available) */ + public const DB_CONVERT_PLACEHOLDER_TARGET = ['pg']; // REGEX_SELECT // REGEX_UPDATE // REGEX INSERT @@ -302,6 +302,9 @@ class IO private string $query = ''; /** @var array current params for query */ private array $params = []; + // if we do have a convert call, store the convert data in here, else it will be empty + /** @var array{}|array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int,matches:array,params_lookup:array,query:string,params:array} */ + private array $placeholder_converted = []; // only inside // basic vars // the dbh handler, if disconnected by command is null, bool:false on error, @@ -333,6 +336,8 @@ class IO private bool $db_convert_placeholder = false; /** @var string convert placeholders target, default is 'pg', other allowed is 'pdo' */ private string $db_convert_placeholder_target = 'pg'; + /** @var bool Replace the placeholders in a query for debug output, defaults to false */ + private bool $db_debug_replace_placeholder = false; // convert type settings // 0: OFF (CONVERT_OFF) // >0: ON @@ -374,6 +379,7 @@ class IO private string $warning_id; /** @var string */ private string $error_history_id; + // timestamp:string,level:string,id:string,error:string,source:string,pg_error:string,message:string,context:array /** @var array Stores warning and errors combinded with detail info */ private array $error_history_long = []; /** @var bool error thrown on class init if we cannot connect to db */ @@ -413,7 +419,7 @@ class IO * and failure set on failed connection * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config DB configuration array + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string,db_debug_replace_placeholder?:bool} $db_config DB configuration array * @param \CoreLibs\Logging\Logging $log Logging class * @throws \RuntimeException If no DB connection can be established on launch */ @@ -442,15 +448,15 @@ class IO '11' => 'No Querystring given', '12' => 'No Cursor given, no correct query perhaps?', '13' => 'Query could not be executed without errors', - '14' => 'Can\'t connect to DB server', - '15' => 'Can\'t select DB or no db name given', - '16' => 'No DB Handler found / connect or reconnect failed', + '14' => 'No DB Handler found / connect or reconnect failed', + '15' => 'Cannot select DB or no db name given', + // '16' => 'No DB Handler found / connect or reconnect failed', // 16 merged into 14 '17' => 'All dbReturn* methods work only with SELECT statements, ' . 'please use dbExec for everything else', '18' => 'Query not found in cache. Nothing has been reset', '19' => 'Wrong PK name given or no PK name given at all, can\'t get Insert ID', - '20' => 'Found given Prepare Statement Name in array, ' - . 'Query not prepared, will use existing one', + '20' => 'Query has already been prepared', + '26' => 'Same prepare statement name has been used for a different query', '21' => 'Query Prepare failed', '22' => 'Query Execute failed', '23' => 'Query Execute failed, data array does not match placeholders', @@ -481,6 +487,10 @@ class IO '104' => 'No Key with this name in the prepared cursor array', // abort on Placeholder convert '200' => 'Cannot have named, question mark or numbered placeholders in the same query', + '210' => 'Cannot lookup param named in param list', + '211' => 'Cannot lookup param named in param lookup list', + '220' => 'Cannot lookup param number in param list', + '221' => 'Cannot lookup param number in param lookup list', ]; // load the core DB functions wrapper class @@ -493,7 +503,6 @@ class IO // connect to DB if (!$this->__connectToDB()) { - $this->__dbError(16); $this->db_connection_closed = true; throw new \RuntimeException('INIT: No DB Handler found / connect or reconnect failed', 16); } @@ -515,7 +524,7 @@ class IO * Setup DB config and options * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string,db_debug_replace_placeholder?:bool} $db_config * @return bool */ private function __setConfigOptions(array $db_config): bool @@ -558,7 +567,10 @@ class IO $this->__setConvertType($db_convert_type); } // set placeholder convert flag and target - if (isset($db_config['db_convert_placeholder']) && is_bool($db_config['db_convert_placeholder'])) { + if ( + isset($db_config['db_convert_placeholder']) && + is_bool($db_config['db_convert_placeholder']) + ) { $this->db_convert_placeholder = $db_config['db_convert_placeholder']; } if ( @@ -568,6 +580,13 @@ class IO $this->db_convert_placeholder_target = $db_config['db_convert_placeholder_target']; } + if ( + isset($db_config['db_debug_replace_placeholder']) && + is_bool($db_config['db_debug_replace_placeholder']) + ) { + $this->db_debug_replace_placeholder = $db_config['db_debug_replace_placeholder']; + } + // return status true: ok, false: options error return true; } @@ -619,7 +638,7 @@ class IO // if non set or none matching abort default: // abort error - $this->__dbError(10); + $this->__dbError(10, context: ['db_type' => $this->db_type]); $this->db_connection_closed = true; break; } @@ -637,7 +656,14 @@ class IO { // no DB name set, abort if (empty($this->db_name)) { - $this->__dbError(15); + $this->__dbError(15, context: [ + 'host' => $this->db_host, + 'user' => $this->db_user, + 'password' => 'sha256:' . hash('sha256', $this->db_pwd), + 'database' => $this->db_name, + 'port' => $this->db_port, + 'ssl' => $this->db_ssl + ]); return false; } // generate connect string @@ -651,7 +677,14 @@ class IO ); // if no dbh here, we couldn't connect to the DB itself if (!$this->dbh) { - $this->__dbError(14); + $this->__dbError(14, context: [ + 'host' => $this->db_host, + 'user' => $this->db_user, + 'password' => 'sha256:' . hash('sha256', $this->db_pwd), + 'database' => $this->db_name, + 'port' => $this->db_port, + 'ssl' => $this->db_ssl + ]); return false; } // 15 error (cant select to DB is not valid in postgres, as connect is different) @@ -686,59 +719,6 @@ class IO } } - /** - * checks if query is a SELECT, SHOW or WITH, if not error, 0 return - * NOTE: - * Query needs to start with SELECT, SHOW or WITH - * - * @param string $query query to check - * @return bool true if matching, false if not - */ - private function __checkQueryForSelect(string $query): bool - { - // change to string starts with? - if (preg_match("/^\s*(?:SELECT|SHOW|WITH)\s/i", $query)) { - return true; - } - return false; - } - - /** - * check for DELETE, INSERT, UPDATE - * if pure is set to true, only when INSERT is set will return true - * NOTE: - * Queries need to start with INSERT, UPDATE, DELETE. Anything else is ignored - * - * @param string $query query to check - * @param bool $pure pure check (only insert), default false - * @return bool true if matching, false if not - */ - private function __checkQueryForInsert(string $query, bool $pure = false): bool - { - if ($pure && preg_match("/^\s*INSERT\s+?INTO\s/i", $query)) { - return true; - } - if (!$pure && preg_match("/^\s*(?:INSERT\s+?INTO|DELETE\s+?FROM|UPDATE)\s/i", $query)) { - return true; - } - return false; - } - - /** - * returns true if the query starts with UPDATE - * query NEEDS to start with UPDATE - * - * @param string $query query to check - * @return bool returns true if the query starts with UPDATE - */ - private function __checkQueryForUpdate(string $query): bool - { - if (preg_match("/^\s*UPDATE\s?(.+)/i", $query)) { - return true; - } - return false; - } - /** * internal funktion that creates the array * NOTE: @@ -899,12 +879,15 @@ class IO * @param \PgSql\Result|false $cursor current cursor for pg_result_error, * pg_last_error too, but pg_result_error * is more accurate (PgSql\Result) + * @param bool $force_log [=false] if we want to log this error to log, on default logging is handled in the + * dbError/dbWarning calls * @return array Pos 0: if we could get the method where it was called * if not found [Uknown Method] * Pos 1: if we have the pg_error_string from last error * if nothing then empty string + * Pos 2: context array for detailed logging */ - private function __dbErrorPreprocessor(\PgSql\Result|false $cursor = false): array + private function __dbErrorPreprocessor(\PgSql\Result|false $cursor = false, bool $force_log = false): array { $db_prefix = ''; $db_error_string = ''; @@ -941,12 +924,20 @@ class IO } elseif (!empty($db_error_string)) { $db_error_string = $db_prefix . ' ' . $db_error_string; } - if ($db_error_string) { + if ($db_error_string && $force_log) { $this->__dbDebugMessage('db', $db_error_string, 'DB_ERROR', $where_called); } + $context = []; + if ($db_error_string) { + $context = [ + 'pg_error_string' => $db_error_string, + 'where_called' => $where_called, + ]; + } return [ $where_called, - $db_error_string + $db_error_string, + $context ]; } @@ -957,11 +948,12 @@ class IO * additional pg error message if exists and optional msg given on error call * all error messages are grouped by error_history_id set when errors are reset * - * @param string $level - * @param string $error_id - * @param string $where_called - * @param string $pg_error_string - * @param string $msg + * @param string $level warning or error + * @param string $error_id error id + * @param string $where_called context wher eerror was colled + * @param string $pg_error_string if set, postgresql error string + * @param string $message additional message + * @param array $context array with more context information (eg query, params, etc) * @return void */ private function __dbErrorHistory( @@ -969,7 +961,8 @@ class IO string $error_id, string $where_called, string $pg_error_string, - string $msg + string $message, + array $context ): void { if (empty($this->error_history_id)) { $this->error_history_id = Uids::uniqId(self::ERROR_HASH_TYPE); @@ -981,7 +974,8 @@ class IO 'error' => $this->error_string[$error_id] ?? '[UNKNOWN ERROR]', 'source' => $where_called, 'pg_error' => $pg_error_string, - 'msg' => $msg, + 'message' => $message, + 'context' => $context, ]; } @@ -990,54 +984,60 @@ class IO * * @param integer $error_id Any Error ID, used in debug message string * @param \PgSql\Result|false $cursor Optional cursor, passed on to preprocessor - * @param string $msg optional message added to debug + * @param string $message Optional message added to debug + * @param array $context Optional Context array, passed on as error_data to the main error * @return void */ protected function __dbError( int $error_id, \PgSql\Result|false $cursor = false, - string $msg = '' + string $message = '', + array $context = [] ): void { $error_id = (string)$error_id; - [$where_called, $pg_error_string] = $this->__dbErrorPreprocessor($cursor); + [$where_called, $pg_error_string, $_context] = $this->__dbErrorPreprocessor($cursor); // write error msg ... $this->__dbDebugMessage( 'db', $error_id . ': ' . ($this->error_string[$error_id] ?? '[UNKNOWN ERROR]') - . ($msg ? ', ' . $msg : ''), + . ($message ? ', ' . $message : ''), 'DB_ERROR', - $where_called + $where_called, + array_merge($context, $_context) ); $this->error_id = $error_id; - // keep warning history - $this->__dbErrorHistory('error', $error_id, $where_called, $pg_error_string, $msg); + // keep error history + $this->__dbErrorHistory('error', $error_id, $where_called, $pg_error_string, $message, $context); } /** * write a warning * - * @param integer $warning_id Integer warning id added to debug + * @param integer $warning_id Integer warning id added to debug * @param \PgSql\Result|false $cursor Optional cursor, passed on to preprocessor - * @param string $msg optional message added to debug + * @param string $message Optional message added to debug + * @param array $context Optional Context array, passed on as error_data to the main error * @return void */ protected function __dbWarning( int $warning_id, \PgSql\Result|false $cursor = false, - string $msg = '' + string $message = '', + array $context = [] ): void { $warning_id = (string)$warning_id; - [$where_called, $pg_error_string] = $this->__dbErrorPreprocessor($cursor); + [$where_called, $pg_error_string, $_context] = $this->__dbErrorPreprocessor($cursor); $this->__dbDebugMessage( 'db', $warning_id . ': ' . ($this->error_string[$warning_id] ?? '[UNKNOWN WARNING') - . ($msg ? ', ' . $msg : ''), + . ($message ? ', ' . $message : ''), 'DB_WARNING', - $where_called + $where_called, + array_merge($context, $_context) ); $this->warning_id = $warning_id; // keep warning history - $this->__dbErrorHistory('warning', $warning_id, $where_called, $pg_error_string, $msg); + $this->__dbErrorHistory('warning', $warning_id, $where_called, $pg_error_string, $message, $context); } /** @@ -1138,39 +1138,69 @@ class IO /** * for debug purpose replaces $1, $2, etc with actual data + * TODO: :name and ? params + * Also works with :name parameters + * ? parameters, will be ignored * * @param string $query Query to replace values in - * @param array $data The data array + * @param array $params The data param array * @return string string of query with data inside */ - private function __dbDebugPrepare(string $query, array $data = []): string + private function __dbDebugPrepare(string $query, array $params = []): string { // skip anything if there is no data - if ($data === []) { + if ($params === []) { return $query; } // get the keys from data array - $keys = array_keys($data); + $keys = array_keys($params); + // check if there is ? or :name i the keys list // because the placeholders start with $ and at 1, // we need to increase each key and prefix it with a $ char for ($i = 0, $iMax = count($keys); $i < $iMax; $i++) { // note: if I use $ here, the str_replace will // replace it again. eg $11 '$1'1would be replaced with $1 again // prefix data set with parameter pos - $data[$i] = '#' . ($keys[$i] + 1) . ':' . ($data[$i] === null ? - '"NULL"' : (string)$data[$i] + $params[$i] = '#' . ($keys[$i] + 1) . ':' . ($params[$i] === null ? + '"NULL"' : (string)$params[$i] ); // search part $keys[$i] = '$' . ($keys[$i] + 1); } // simply replace the $1, $2, ... with the actual data and return it + // note that we do this in return to go from highest number to lowest return str_replace( array_reverse($keys), - array_reverse($data), + array_reverse($params), $query ); } + /** + * Created the context/error_data error for debug messages + * + * @param string $query Query called + * @param array $params Params + * @return array{}|array Empty array if no params, or params + * with optional prepared statement + */ + private function __dbDebugPrepareContext(string $query, array $params = []): array + { + if ($this->params === []) { + return []; + } + $error_data = [ + 'params' => $params + ]; + if ($this->dbGetDebugReplacePlaceholder()) { + $error_data['prepared'] = $this->__dbDebugPrepare( + $query, + $params + ); + } + return $error_data; + } + /** * extracts schema and table from the query, * if no schema returns just empty string @@ -1182,7 +1212,7 @@ class IO { $matches = []; $schema_table = []; - if ($this->__checkQueryForSelect($query)) { + if ($this->dbCheckQueryForSelect($query)) { // only selects the first one, this is more a fallback // MATCHES 1 (call), 3 (schema), 4 (table) preg_match("/\s+?(FROM)\s+?([\"'])?(?:([\w_]+)\.)?([\w_]+)(?:\2)?\s?/i", $query, $matches); @@ -1306,20 +1336,25 @@ class IO * Checks if the placeholder count in the query matches the params given * on call * - * @param string $query Query to check - * @param int $params_count The parms count expected + * @param string $query Query to check + * @param array $params The parms to count count expected * @return bool True for params count ok, else false */ - private function __dbCheckQueryParams(string $query, int $params_count): bool + private function __dbCheckQueryParams(string $query, array $params): bool { $placeholder_count = $this->__dbCountQueryParams($query); + $params_count = count($params); if ($params_count != $placeholder_count) { $this->__dbError( 23, false, - 'Array data count does not match prepared fields. Need: ' - . $placeholder_count . ', has: ' - . $params_count + 'Need: ' . $placeholder_count . ', has: ' . $params_count, + [ + 'query' => $query, + 'params' => $params, + 'placeholder_needed' => $placeholder_count, + 'placeholder_provided' => $params_count, + ] ); return false; } @@ -1366,7 +1401,6 @@ class IO if (!$this->dbh) { // if reconnect fails drop out if (!$this->__connectToDB()) { - $this->__dbError(16); return false; } } @@ -1376,7 +1410,7 @@ class IO } // if we do have an insert, check if there is no RETURNING pk_id, // add it if I can get the PK id - if ($this->__checkQueryForInsert($this->query, true)) { + if ($this->dbCheckQueryForInsert($this->query, true)) { $this->pk_name = $pk_name; if ($this->pk_name != 'NULL') { if (!$this->pk_name) { @@ -1414,7 +1448,7 @@ class IO } // if we have an UPDATE and RETURNING, flag for true, but do not add anything if ( - $this->__checkQueryForUpdate($this->query) && + $this->dbCheckQueryForUpdate($this->query) && preg_match(self::REGEX_RETURNING, $this->query, $matches) ) { $this->returning_id = true; @@ -1423,23 +1457,46 @@ class IO $query_hash = $this->dbGetQueryHash($this->query, $this->params); // QUERY PARAMS: run query params check and rewrite if ($this->dbGetConvertPlaceholder() === true) { - $convert = ConvertPlaceholder::convertPlaceholderInQuery( - $query, - $params, - $this->dbGetConvertPlaceholderTarget() - ); + try { + $this->placeholder_converted = ConvertPlaceholder::convertPlaceholderInQuery( + $this->query, + $this->params, + $this->dbGetConvertPlaceholderTarget() + ); + // write the new queries over the old + if (!empty($this->placeholder_converted['query'])) { + $this->query = $this->placeholder_converted['query']; + $this->params = $this->placeholder_converted['params']; + } + } catch (\OutOfRangeException $e) { + $this->__dbError($e->getCode(), context:[ + 'query' => $this->query, + 'params' => $this->params, + 'location' => '__dbPrepareExec', + 'error' => 'OutOfRangeException', + 'exception' => $e + ]); + return false; + } catch (\RuntimeException $e) { + $this->__dbError($e->getCode(), context:[ + 'query' => $this->query, + 'params' => $this->params, + 'location' => '__dbPrepareExec', + 'error' => 'RuntimeException', + 'exception' => $e + ]); + return false; + } } // $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id); // for DEBUG, only on first time ;) $this->__dbDebug( 'db', - $this->__dbDebugPrepare( - $this->query, - $this->params - ), + $this->query, '__dbPrepareExec', - ($this->params === [] ? 'Q' : 'Qp') + ($this->params === [] ? 'Q' : 'Qp'), + error_data: $this->__dbDebugPrepareContext($this->query, $this->params) ); // if the array index does not exists set it 0 if (!array_key_exists($query_hash, $this->query_called)) { @@ -1455,16 +1512,11 @@ class IO $this->MAX_QUERY_CALL != -1 && $this->query_called[$query_hash] > $this->MAX_QUERY_CALL ) { - $this->__dbError(30, false, $this->query); - $this->__dbDebugMessage( - 'db', - $this->__dbDebugPrepare( - $this->query, - $this->params - ), - 'dbExec', - ($this->params === [] ? 'Q[nc]' : 'Qp[nc]') - ); + $this->__dbError(30, false, context: [ + 'query' => $this->query, + 'params' => $this->params, + 'location' => '__dbPrepareExec' + ]); return false; } $this->query_called[$query_hash] ++; @@ -1484,14 +1536,17 @@ class IO // if FALSE returned, set error stuff // if either the cursor is false if ($this->cursor === false || $this->db_functions->__dbLastErrorQuery()) { - // printout Query if debug is turned on - $this->__dbDebug('db', $this->query, 'dbExec', 'Q[nc]'); // internal error handling - $this->__dbError(13, $this->cursor); + $this->__dbError(13, $this->cursor, context: [ + 'query' => $this->query, + 'params' => $this->params, + 'location' => 'dbExec', + 'query_id' => 'Q[nc]', + ]); return false; } else { // if SELECT do here ... - if ($this->__checkQueryForSelect($this->query)) { + if ($this->dbCheckQueryForSelect($this->query)) { // count the rows returned (if select) $this->num_rows = $this->db_functions->__dbNumRows($this->cursor); // count the fields @@ -1512,15 +1567,15 @@ class IO $this->field_names, $this->field_types ); - } elseif ($this->__checkQueryForInsert($this->query)) { + } elseif ($this->dbCheckQueryForInsert($this->query)) { // if not select do here // count affected rows $this->num_rows = $this->db_functions->__dbAffectedRows($this->cursor); if ( // ONLY insert with set pk name - ($this->__checkQueryForInsert($this->query, true) && $this->pk_name != 'NULL') || + ($this->dbCheckQueryForInsert($this->query, true) && $this->pk_name != 'NULL') || // insert or update with returning add - ($this->__checkQueryForInsert($this->query) && $this->returning_id) + ($this->dbCheckQueryForInsert($this->query) && $this->returning_id) ) { $this->__dbSetInsertId( $this->returning_id, @@ -1890,6 +1945,63 @@ class IO return $string; } + // *************************** + // CHECK QUERY TYPE + // *************************** + + /** + * checks if query is a SELECT, SHOW or WITH, if not error, 0 return + * NOTE: + * Query needs to start with SELECT, SHOW or WITH + * + * @param string $query query to check + * @return bool true if matching, false if not + */ + public function dbCheckQueryForSelect(string $query): bool + { + // change to string starts with? + if (preg_match("/^\s*(?:SELECT|SHOW|WITH)\s/i", $query)) { + return true; + } + return false; + } + + /** + * check for DELETE, INSERT, UPDATE + * if pure is set to true, only when INSERT is set will return true + * NOTE: + * Queries need to start with INSERT, UPDATE, DELETE. Anything else is ignored + * + * @param string $query query to check + * @param bool $pure pure check (only insert), default false + * @return bool true if matching, false if not + */ + public function dbCheckQueryForInsert(string $query, bool $pure = false): bool + { + if ($pure && preg_match("/^\s*INSERT\s+?INTO\s/i", $query)) { + return true; + } + if (!$pure && preg_match("/^\s*(?:INSERT\s+?INTO|DELETE\s+?FROM|UPDATE)\s/i", $query)) { + return true; + } + return false; + } + + /** + * returns true if the query starts with UPDATE + * query NEEDS to start with UPDATE + * + * @param string $query query to check + * @return bool returns true if the query starts with UPDATE + */ + public function dbCheckQueryForUpdate(string $query): bool + { + if (preg_match("/^\s*UPDATE\s?(.+)/i", $query)) { + return true; + } + return false; + } + // *************************** // DATA WRITE CONVERSION // *************************** @@ -2136,7 +2248,10 @@ class IO $table = (!empty($schema) ? $schema . '.' : '') . $table; $array = $this->db_functions->__dbMetaData($table); if (!is_array($array)) { - $this->__dbError(60); + $this->__dbError(60, context: [ + 'table' => $table, + 'schema' => $schema + ]); $array = false; } return $array; @@ -2245,6 +2360,8 @@ class IO 'query' => '', // parameter 'params' => [], + // if we convert placeholders, conversion data is stored here + 'placeholder_converted' => [], // cache flag from method call 'cache_flag' => $cache, // flag if we only have assoc data @@ -2262,44 +2379,71 @@ class IO 'log_pos' => 1, // how many times called overall 'log' => [], // current run log ]; + + // set the query + $this->cursor_ext[$query_hash]['query'] = $query; + // before doing ANYTHING check if query is "SELECT ..." everything else does not work + if (!$this->dbCheckQueryForSelect($this->cursor_ext[$query_hash]['query'])) { + $this->__dbError(17, false, context: [ + 'query' => $this->cursor_ext[$query_hash]['query'], + 'params' => $this->cursor_ext[$query_hash]['params'], + 'location' => 'dbReturn', + ]); + return false; + } + // set the query parameters + $this->cursor_ext[$query_hash]['params'] = $params; + // QUERY PARAMS: run query params check and rewrite + if ($this->dbGetConvertPlaceholder() === true) { + try { + $this->cursor_ext[$query_hash]['placeholder_converted'] = + ConvertPlaceholder::convertPlaceholderInQuery( + $this->cursor_ext[$query_hash]['query'], + $this->cursor_ext[$query_hash]['params'], + $this->dbGetConvertPlaceholderTarget() + ); + if (!empty($this->cursor_ext[$query_hash]['placeholder_converted']['query'])) { + $this->cursor_ext[$query_hash]['query'] = + $this->cursor_ext[$query_hash]['placeholder_converted']['query']; + $this->cursor_ext[$query_hash]['params'] = + $this->cursor_ext[$query_hash]['placeholder_converted']['params']; + } + } catch (\OutOfRangeException $e) { + $this->__dbError($e->getCode(), context:[ + 'query' => $this->cursor_ext[$query_hash]['query'], + 'params' => $this->cursor_ext[$query_hash]['params'], + 'location' => 'dbReturn', + 'error' => 'OutOfRangeException', + 'exception' => $e + ]); + return false; + } catch (\RuntimeException $e) { + $this->__dbError($e->getCode()); + $this->__dbError($e->getCode(), context:[ + 'query' => $this->cursor_ext[$query_hash]['query'], + 'params' => $this->cursor_ext[$query_hash]['params'], + 'location' => 'dbReturn', + 'error' => 'RuntimeException', + 'exception' => $e + ]); + return false; + } + } + // check if params count matches + // checks if the params count given matches the expected count + if ( + $this->__dbCheckQueryParams( + $this->cursor_ext[$query_hash]['query'], + $this->cursor_ext[$query_hash]['params'] + ) === false + ) { + return false; + } } else { $this->cursor_ext[$query_hash]['log_pos'] ++; } // reset log for each read $this->cursor_ext[$query_hash]['log'] = []; - // set the query - $this->cursor_ext[$query_hash]['query'] = $query; - // before doing ANYTHING check if query is "SELECT ..." everything else does not work - if (!$this->__checkQueryForSelect($this->cursor_ext[$query_hash]['query'])) { - $this->__dbError(17, false, $this->cursor_ext[$query_hash]['query']); - return false; - } - // QUERY PARAMS: run query params check and rewrite - if ($this->dbGetConvertPlaceholder() === true) { - $convert = ConvertPlaceholder::convertPlaceholderInQuery( - $query, - $params, - $this->dbGetConvertPlaceholderTarget() - ); - } - - // set the query parameters - $this->cursor_ext[$query_hash]['params'] = $params; - // check if params count matches - // checks if the params count given matches the expected count - if ($this->__dbCheckQueryParams($query, count($params)) === false) { - // in case we got an error print out query - $this->__dbDebug( - 'db', - $this->__dbDebugPrepare( - $this->query, - $this->params - ), - 'dbReturn', - ($this->params === [] ? 'Q[e]' : 'Qp[e]') - ); - return false; - } // set first call to false $first_call = false; // init return als false @@ -2322,18 +2466,18 @@ class IO // for DEBUG, print out each query executed $this->__dbDebug( 'db', - $this->__dbDebugPrepare( - $this->cursor_ext[$query_hash]['query'], - $this->cursor_ext[$query_hash]['params'] - ), + $this->cursor_ext[$query_hash]['query'], 'dbReturn', ($this->cursor_ext[$query_hash]['params'] === [] ? 'Q' : 'Qp'), + error_data: $this->__dbDebugPrepareContext( + $this->cursor_ext[$query_hash]['query'], + $this->cursor_ext[$query_hash]['params'] + ) ); // if no DB Handler try to reconnect if (!$this->dbh) { // if reconnect fails drop out if (!$this->__connectToDB()) { - $this->__dbError(16); return false; } } @@ -2355,17 +2499,12 @@ class IO } // if still no cursor ... if (!$this->cursor_ext[$query_hash]['cursor']) { - $this->__dbDebug( - 'db', - $this->__dbDebugPrepare( - $this->cursor_ext[$query_hash]['query'], - $this->cursor_ext[$query_hash]['params'] - ), - 'dbReturn', - ($this->cursor_ext[$query_hash]['params'] === [] ? 'Q[e]' : 'Qp[e]'), - ); // internal error handling - $this->__dbError(13, $this->cursor_ext[$query_hash]['cursor']); + $this->__dbError(13, $this->cursor_ext[$query_hash]['cursor'], context: [ + 'query' => $this->cursor_ext[$query_hash]['query'], + 'params' => $this->cursor_ext[$query_hash]['params'], + 'location' => 'dbReturn' + ]); return false; } else { $first_call = true; @@ -2580,19 +2719,23 @@ class IO return false; } // checks if the params count given matches the expected count - if ($this->__dbCheckQueryParams($query, count($params)) === false) { + if ($this->__dbCheckQueryParams($this->query, $this->params) === false) { return false; } // ** actual db exec call - if ($params === []) { + if ($this->params === []) { $cursor = $this->db_functions->__dbQuery($this->query); } else { - $cursor = $this->db_functions->__dbQueryParams($this->query, $params); + $cursor = $this->db_functions->__dbQueryParams($this->query, $this->params); } // if we faield, just set the master cursors to false too $this->cursor = $cursor; if ($cursor === false) { - $this->__dbError(13); + $this->__dbError(13, context: [ + 'query' => $this->query, + 'params' => $this->params, + 'location' => 'dbExecParams', + ]); return false; } // if FALSE returned, set error stuff @@ -2672,8 +2815,13 @@ class IO } // before doing ANYTHING check if query is // "SELECT ..." everything else does not work - if (!$this->__checkQueryForSelect($query)) { - $this->__dbError(17, false, $query); + if (!$this->dbCheckQueryForSelect($query)) { + $this->__dbError(17, false, context: [ + 'query' => $query, + 'params' => $params, + 'assoc_only' => $assoc_only, + 'location' => 'dbReturnRowParams' + ]); return false; } $cursor = $this->dbExecParams($query, $params); @@ -2717,8 +2865,13 @@ class IO return false; } // before doing ANYTHING check if query is "SELECT ..." everything else does not work - if (!$this->__checkQueryForSelect($query)) { - $this->__dbError(17, false, $query); + if (!$this->dbCheckQueryForSelect($query)) { + $this->__dbError(17, false, context: [ + 'query' => $query, + 'params' => $params, + 'assoc_only' => $assoc_only, + 'location' => 'dbReturnArrayParams' + ]); return false; } $cursor = $this->dbExecParams($query, $params); @@ -2763,7 +2916,11 @@ class IO $query_hash = $this->dbGetQueryHash($query, $params); // clears cache for this query if (empty($this->cursor_ext[$query_hash]['query'])) { - $this->__dbError(18); + $this->__dbError(18, context: [ + 'query' => $query, + 'params' => $params, + 'hash' => $query_hash, + ]); return false; } unset($this->cursor_ext[$query_hash]); @@ -2932,7 +3089,6 @@ class IO if (!$this->dbh) { // if reconnect fails drop out if (!$this->__connectToDB()) { - $this->__dbError(16); return false; } } @@ -2959,7 +3115,7 @@ class IO 'returning_id' => false ]; // if this is an insert query, check if we can add a return - if ($this->__checkQueryForInsert($query, true)) { + if ($this->dbCheckQueryForInsert($query, true)) { if ($pk_name != 'NULL') { // set primary key name // current: only via parameter @@ -3007,14 +3163,34 @@ class IO $this->__dbError( 21, false, - $stm_name . ': Prepare field with: ' . $stm_name . ' | ' . $query + context: [ + 'statement_name' => $stm_name, + 'query' => $query, + 'pk_name' => $pk_name, + ] ); return $result; } } else { - // thrown warning - $this->__dbWarning(20, false, $stm_name); - return true; + // if we try to use the same statement name for a differnt query, error abort + if ($this->prepare_cursor[$stm_name]['query'] != $query) { + // thrown error + $this->__dbError(26, false, context: [ + 'statement_name' => $stm_name, + 'prepared_query' => $this->prepare_cursor[$stm_name]['query'], + 'query' => $query, + 'pk_name' => $pk_name, + ]); + return false; + } else { + // thrown warning + $this->__dbWarning(20, false, context: [ + 'statement_name' => $stm_name, + 'query' => $query, + 'pk_name' => $pk_name, + ]); + return true; + } } } @@ -3032,7 +3208,6 @@ class IO if (!$this->dbh) { // if reconnect fails drop out if (!$this->__connectToDB()) { - $this->__dbError(16); return false; } } @@ -3054,28 +3229,37 @@ class IO $this->__dbError( 24, false, - $stm_name . ': We do not have a prepared query entry for this statement name.' + $stm_name . ': We do not have a prepared query entry for this statement name.', + context: ['statement_name' => $stm_name] ); return false; } $this->__dbDebug( 'db', - $this->__dbDebugPrepare( + $this->prepare_cursor[$stm_name]['query'], + 'dbExecute', + 'Qpe', + error_data: array_merge([ + 'statement_name' => $stm_name, + ], $this->__dbDebugPrepareContext( $this->prepare_cursor[$stm_name]['query'], $data - ), - 'dbExecPrep', - 'Qpe' + )) ); // if the count does not match if ($this->prepare_cursor[$stm_name]['count'] != count($data)) { $this->__dbError( 23, false, - $stm_name - . ': Array data count does not match prepared fields. Need: ' - . $this->prepare_cursor[$stm_name]['count'] . ', has: ' - . count($data) + '(' . $stm_name . ') ' + . 'Need: ' . $this->prepare_cursor[$stm_name]['count'] . ', has: ' . count($data), + context: [ + 'statement_name' => $stm_name, + 'query' => $this->prepare_cursor[$stm_name]['query'], + 'params' => $data, + 'placeholder_needed' => $this->prepare_cursor[$stm_name]['count'], + 'placeholder_provided' => count($data) + ] ); return false; } @@ -3087,16 +3271,20 @@ class IO $this->__dbError( 22, $this->prepare_cursor[$stm_name]['result'], - $stm_name . ': Execution failed' + context: [ + 'statement_name' => $stm_name, + 'query' => $this->prepare_cursor[$stm_name]['query'], + 'params' => $data + ] ); return false; } if ( // pure insert wth pk name - ($this->__checkQueryForInsert($this->prepare_cursor[$stm_name]['query'], true) && + ($this->dbCheckQueryForInsert($this->prepare_cursor[$stm_name]['query'], true) && $this->prepare_cursor[$stm_name]['pk_name'] != 'NULL') || // insert or update with returning set - ($this->__checkQueryForInsert($this->prepare_cursor[$stm_name]['query']) && + ($this->dbCheckQueryForInsert($this->prepare_cursor[$stm_name]['query']) && $this->prepare_cursor[$stm_name]['returning_id'] === true ) ) { @@ -3157,20 +3345,24 @@ class IO return false; } // checks if the params count given matches the expected count - if ($this->__dbCheckQueryParams($query, count($params)) === false) { + if ($this->__dbCheckQueryParams($this->query, $this->params) === false) { return false; } // ** actual db exec call if ($params === []) { $status = $this->db_functions->__dbSendQuery($this->query); } else { - $status = $this->db_functions->__dbSendQueryParams($this->query, $params); + $status = $this->db_functions->__dbSendQueryParams($this->query, $this->params); } // run the async query, this just returns true or false // the actually result is in dbCheckAsync if (!$status) { // if failed, process here - $this->__dbError(40); + $this->__dbError(40, context: [ + 'query' => $this->query, + 'params' => $this->params, + 'pk_name' => $pk_name, + ]); return false; } else { $this->async_running = (string)$query_hash; @@ -3251,8 +3443,7 @@ class IO // if no async running print error $this->__dbError( 42, - false, - 'No async query has been started yet.' + false ); return false; } @@ -3589,6 +3780,27 @@ class IO return $this->db_convert_placeholder_target; } + /** + * Set flag if we print the query with replaced placeholders or not + * + * @param bool $flag + * @return void + */ + public function dbSetDebugReplacePlaceholder(bool $flag): void + { + $this->db_debug_replace_placeholder = $flag; + } + + /** + * get the current setting for the debug replace placeholder + * + * @return bool True for replace query, False for not + */ + public function dbGetDebugReplacePlaceholder(): bool + { + return $this->db_debug_replace_placeholder; + } + /** * set max query calls, set to -1 to disable loop * protection. this will generate a warning @@ -3606,11 +3818,11 @@ class IO // if -1 then disable loop check // DANGEROUS, WARN USER if ($max_calls == -1) { - $this->__dbWarning(50); + $this->__dbWarning(50, context: ['max_calls' => $max_calls]); } // negative or 0 if ($max_calls < -1 || $max_calls == 0) { - $this->__dbError(51); + $this->__dbError(51, context: ['max_calls' => $max_calls]); // early abort return false; } @@ -3654,7 +3866,7 @@ class IO case 2: // setting schema failed (3) case 3: - $this->__dbError(71); + $this->__dbError(71, context: ['schema' => $db_schema]); $status = false; break; } @@ -3704,7 +3916,7 @@ class IO case 2: // 3 is set failed case 3: - $this->__dbError(81); + $this->__dbError(81, context: ['encoding' => $db_encoding]); $status = false; break; } @@ -3829,6 +4041,16 @@ class IO $this->params = []; } + /** + * Returns the placeholder convert set or empty + * + * @return array{}|array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int,matches:array,params_lookup:array,query:string,params:array} + */ + public function dbGetPlaceholderConverted(): array + { + return $this->placeholder_converted; + } + // *************************** // INTERNAL VARIABLES READ POST QUERY RUN // *************************** @@ -4047,7 +4269,8 @@ class IO $this->__dbError( 102, false, - 'Invalid key name' + 'Invalid key name', + context: ['key' => $key] ); return false; } @@ -4056,7 +4279,8 @@ class IO $this->__dbError( 103, false, - 'Statement name does not exist in prepare cursor array' + 'Statement name does not exist in prepare cursor array', + context: ['statement_name' => $stm_name] ); return false; } @@ -4065,7 +4289,11 @@ class IO $this->__dbError( 104, false, - 'Key does not exist in prepare cursor array' + 'Key does not exist in prepare cursor array', + context: [ + 'statement_name' => $stm_name, + 'key' => $key + ] ); return false; } diff --git a/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php b/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php index 0c194dde..bcfd5613 100644 --- a/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php +++ b/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php @@ -24,10 +24,12 @@ class ConvertPlaceholder * * If the convert_to is either pg or pdo, nothing will be changed * + * found has -1 if an error occoured in the preg_match_all call + * * @param string $query Query with placeholders to convert * @param array $params The parameters that are used for the query, and will be updated * @param string $convert_to Either pdo or pg, will be converted to lower case for check - * @return array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int|false,matches:array,params_lookup:array,query:string,params:array} + * @return array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int,matches:array,params_lookup:array,query:string,params:array} * @throws \OutOfRangeException 200 */ public static function convertPlaceholderInQuery( @@ -61,14 +63,27 @@ class ConvertPlaceholder // 2: ? question mark // 3: $n numbered $found = preg_match_all($pattern, $query, $matches, PREG_UNMATCHED_AS_NULL); + // if false or null set to -1 + // || $found === null + if ($found === false) { + $found = -1; + } /** @var array 1: named */ $named_matches = array_filter($matches[1]); /** @var array 2: open ? */ $qmark_matches = array_filter($matches[2]); /** @var array 3: $n matches */ $numbered_matches = array_filter($matches[3]); + // count matches + $count_named = count($named_matches); + $count_qmark = count($qmark_matches); + $count_numbered = count($numbered_matches); // throw if mixed - if (count($named_matches) && count($qmark_matches) && count($numbered_matches)) { + if ( + ($count_named && $count_qmark) || + ($count_named && $count_numbered) || + ($count_qmark && $count_numbered) + ) { throw new \OutOfRangeException('Cannot have named, question mark and numbered in the same query', 200); } // rebuild @@ -77,7 +92,7 @@ class ConvertPlaceholder $query_new = ''; $params_new = []; $params_lookup = []; - if (count($named_matches) && $convert_to == 'pg') { + if ($count_named && $convert_to == 'pg') { $type = 'named'; $matches_return = $named_matches; // only check for :named @@ -113,7 +128,7 @@ class ConvertPlaceholder }, $query ); - } elseif (count($qmark_matches) && $convert_to == 'pg') { + } elseif ($count_qmark && $convert_to == 'pg') { $type = 'question_mark'; $matches_return = $qmark_matches; // order and data stays the same @@ -143,7 +158,7 @@ class ConvertPlaceholder $query ); // for each ?:DTN: -> replace with $1 ... $n, any remaining :DTN: remove - } elseif (count($numbered_matches) && $convert_to == 'pdo') { + } elseif ($count_numbered && $convert_to == 'pdo') { // convert numbered to named $type = 'numbered'; $matches_return = $numbered_matches; @@ -190,8 +205,8 @@ class ConvertPlaceholder ], // type found, empty if nothing was done 'type' => $type, - // int|null: found, not found; false: problem - 'found' => $found, + // int: found, not found; -1: problem (set from false) + 'found' => (int)$found, 'matches' => $matches_return, // old to new lookup check 'params_lookup' => $params_lookup,