Initial add for PDO/PG convert placeholders

This commit is contained in:
Clemens Schwaighofer
2023-10-11 18:36:06 +09:00
parent 97e1b2b63d
commit ae2d6580a2
8 changed files with 332 additions and 13 deletions

View File

@@ -0,0 +1,31 @@
# DB Query Params ? and : to $
dbReturn*
dbExec
keep
->query
->params
for reference
## : named params
in order for each named found replace with order number:
```txt
:name, :foo, :bar, :name =>
$1, $2, $3, $1
```
```php
$query = str_replace(
[':name', ':foo', ':bar'],
['$1', '$2', '$3'],
$query
);
```
## ? Params
Foreach ? set $1 to $n and store that in new params array
in QUERY for each ? replace with matching $n

View File

@@ -44,7 +44,7 @@ class EditBase
* construct form generator
*
* 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_config db config array, mandatory
* @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 config array, mandatory
* @param \CoreLibs\Logging\Logging $log Logging class, null auto set
* @param \CoreLibs\Language\L10n $l10n l10n language class, null auto set
* @param \CoreLibs\ACL\Login $login login class for ACL settings

View File

@@ -55,7 +55,7 @@ class ArrayIO extends \CoreLibs\DB\IO
* primary key name automatically (from array)
*
* 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_config db connection 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_config db connection config
* @param array<mixed> $table_array table array config
* @param string $table_name table name string
* @param \CoreLibs\Logging\Logging $log Logging class

View File

@@ -261,6 +261,7 @@ use CoreLibs\Debug\Support;
use CoreLibs\Create\Uids;
use CoreLibs\Convert\Json;
use CoreLibs\DB\Options\Convert;
use CoreLibs\DB\Support\ConvertPlaceholder;
// below no ignore is needed if we want to use PgSql interface checks with PHP 8.0
// as main system. Currently all @var sets are written as object
@@ -283,6 +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<string> allowed convert target for placeholder */
public const DB_CONVERT_PLACEHOLDER_TARGET = ['pg', 'pdo'];
// REGEX_SELECT
// REGEX_UPDATE
// REGEX INSERT
@@ -326,10 +329,14 @@ class IO
private string $db_ssl;
/** @var array<string> flag for converting types from settings */
private array $db_convert_type = [];
/** @var bool convert placeholders from pdo to Pg or the other way around */
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';
// convert type settings
// 0: OFF (CONVERT_OFF)
// >0: ON
// 1: convert intN/bool (CONVERT_ON)
// 1: convert int/bool (CONVERT_ON)
// 2: convert json/jsonb to array (CONVERT_JSON)
// 4: convert numeric/floatN to float (CONVERT_NUMERIC)
// 8: convert bytea to string data (CONVERT_BYTEA)
@@ -406,7 +413,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_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_config DB configuration array
* @param \CoreLibs\Logging\Logging $log Logging class
* @throws \RuntimeException If no DB connection can be established on launch
*/
@@ -441,8 +448,7 @@ class IO
'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',
'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',
'21' => 'Query Prepare failed',
@@ -472,7 +478,9 @@ class IO
'101' => 'Statement name empty for get prepare cursor',
'102' => 'Key empty for get prepare cursir',
'103' => 'No prepared cursor with this name',
'104' => 'No Key with this name in the prepared cursor array'
'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',
];
// load the core DB functions wrapper class
@@ -507,7 +515,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_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_config
* @return bool
*/
private function __setConfigOptions(array $db_config): bool
@@ -549,6 +557,16 @@ class IO
$this->db_convert_type[] = $db_convert_type;
$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'])) {
$this->db_convert_placeholder = $db_config['db_convert_placeholder'];
}
if (
isset($db_config['db_convert_placeholder_target']) &&
in_array($db_config['db_convert_placeholder_target'], self::DB_CONVERT_PLACEHOLDER_TARGET)
) {
$this->db_convert_placeholder_target = $db_config['db_convert_placeholder_target'];
}
// return status true: ok, false: options error
return true;
@@ -1403,6 +1421,14 @@ class IO
}
// import protection, hash needed
$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()
);
}
// $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id);
// for DEBUG, only on first time ;)
@@ -2248,6 +2274,15 @@ class IO
$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
@@ -2540,7 +2575,7 @@ class IO
): \PgSql\Result|false {
$this->__dbErrorReset();
// prepare and check if we can actually run it
if ($this->__dbPrepareExec($query, $params, $pk_name) === false) {
if (($query_hash = $this->__dbPrepareExec($query, $params, $pk_name)) === false) {
// bail if no query hash set
return false;
}
@@ -3461,7 +3496,7 @@ class IO
}
/**
* Undocumented function
* convert db values (set)
*
* @param Convert $convert
* @return void
@@ -3472,7 +3507,7 @@ class IO
}
/**
* Undocumented function
* unsert convert db values flag
*
* @param Convert $convert
* @return void
@@ -3495,7 +3530,7 @@ class IO
}
/**
* Undocumented function
* check if a conert flag is set
*
* @param Convert $convert
* @return bool
@@ -3508,6 +3543,52 @@ class IO
return false;
}
/**
* Set if we want to auto convert PDO/\Pg placeholders
*
* @param bool $flag
* @return void
*/
public function dbSetConvertPlaceholder(bool $flag): void
{
$this->db_convert_placeholder = $flag;
}
/**
* get the flag status if we want to auto convert placeholders in the query
*
* @return bool
*/
public function dbGetConvertPlaceholder(): bool
{
return $this->db_convert_placeholder;
}
/**
* Set convert target for placeholders, returns false on error, true on ok
*
* @param string $target 'pg' or 'pdo', defined in DB_CONVERT_PLACEHOLDER_TARGET
* @return bool
*/
public function dbSetConvertPlaceholderTarget(string $target): bool
{
if (in_array($target, self::DB_CONVERT_PLACEHOLDER_TARGET)) {
$this->db_convert_placeholder_target = $target;
return true;
}
return false;
}
/**
* Get the current placeholder convert target
*
* @return string
*/
public function dbGetConvertPlaceholderTarget(): string
{
return $this->db_convert_placeholder_target;
}
/**
* set max query calls, set to -1 to disable loop
* protection. this will generate a warning

View File

@@ -0,0 +1,205 @@
<?php
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/10/10
* DESCRIPTION:
* Convert placeholders in query from PDO style ? or :named to \PG style $number
* pr the other way around
*/
declare(strict_types=1);
namespace CoreLibs\DB\Support;
class ConvertPlaceholder
{
/**
* Convert PDO type query with placeholders to \PG style and vica versa
* For PDO to: ? and :named
* For \PG to: $number
*
* If the query has a mix of ?, :named or $numbrer the \OutOfRangeException exception
* will be thrown
*
* If the convert_to is either pg or pdo, nothing will be changed
*
* @param string $query Query with placeholders to convert
* @param array<mixed> $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<mixed>},type:''|'named'|'numbered'|'question_mark',found:int|false,matches:array<string>,params_lookup:array<mixed>,query:string,params:array<mixed>}
* @throws \OutOfRangeException 200
*/
public static function convertPlaceholderInQuery(
string $query,
array $params,
string $convert_to = 'pg'
): array {
$convert_to = strtolower($convert_to);
$matches = [];
$pattern = '/'
// prefix string part, must match towards
. '(?:\'.*?\')?\s*(?:\?\?|[(=,])\s*'
// match for replace part
. '(?:'
// digit -> ignore
. '\d+|'
// other string -> ignore
. '(?:\'.*?\')|'
// :name named part (PDO)
. '(:\w+)|'
// ? question mark part (PDO)
. '(?:(?:\?\?)?\s*(\?{1}))|'
// $n numbered part (\PG php)
. '(\$[1-9]{1}(?:[0-9]{1,})?)'
// end match
. ')'
// single line -> add line break to matches in "."
. '/s';
// matches:
// 1: :named
// 2: ? question mark
// 3: $n numbered
$found = preg_match_all($pattern, $query, $matches, PREG_UNMATCHED_AS_NULL);
/** @var array<string> 1: named */
$named_matches = array_filter($matches[1]);
/** @var array<string> 2: open ? */
$qmark_matches = array_filter($matches[2]);
/** @var array<string> 3: $n matches */
$numbered_matches = array_filter($matches[3]);
// throw if mixed
if (count($named_matches) && count($qmark_matches) && count($numbered_matches)) {
throw new \OutOfRangeException('Cannot have named, question mark and numbered in the same query', 200);
}
// rebuild
$matches_return = [];
$type = '';
$query_new = '';
$params_new = [];
$params_lookup = [];
if (count($named_matches) && $convert_to == 'pg') {
$type = 'named';
$matches_return = $named_matches;
// only check for :named
$pattern_replace = '/((?:\'.*?\')?\s*(?:\?\?|[(=,])\s*)(\d+|(?:\'.*?\')|(:\w+))/s';
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part :named
$pos = 0;
$query_new = preg_replace_callback(
$pattern_replace,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) {
// only count up if $match[3] is not yet in lookup table
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) {
$pos++;
$params_lookup[$matches[3]] = '$' . $pos;
$params_new[] = $params[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params list',
210
);
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
$params_lookup[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params lookup list',
211
)
);
},
$query
);
} elseif (count($qmark_matches) && $convert_to == 'pg') {
$type = 'question_mark';
$matches_return = $qmark_matches;
// order and data stays the same
$params_new = $params;
// only check for ?
$pattern_replace = '/((?:\'.*?\')?\s*(?:\?\?|[(=,])\s*)(\d+|(?:\'.*?\')|(?:(?:\?\?)?\s*(\?{1})))/s';
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part ?
$pos = 0;
$query_new = preg_replace_callback(
$pattern_replace,
function ($matches) use (&$pos, &$params_lookup) {
// only count pos up for actual replacements we will do
if (!empty($matches[3])) {
$pos++;
$params_lookup[] = '$' . $pos;
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
'$' . $pos
);
},
$query
);
// for each ?:DTN: -> replace with $1 ... $n, any remaining :DTN: remove
} elseif (count($numbered_matches) && $convert_to == 'pdo') {
// convert numbered to named
$type = 'numbered';
$matches_return = $numbered_matches;
// only check for $n
$pattern_replace = '/((?:\'.*?\')?\s*(?:\?\?|[(=,])\s*)(\d+|(?:\'.*?\')|(\$[1-9]{1}(?:[0-9]{1,})?))/s';
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part $numbered
$pos = 0;
$query_new = preg_replace_callback(
$pattern_replace,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) {
// only count up if $match[3] is not yet in lookup table
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) {
$pos++;
$params_lookup[$matches[3]] = ':' . $pos . '_named';
$params_new[] = $params[($pos - 1)] ??
throw new \RuntimeException(
'Cannot lookup ' . ($pos - 1) . ' in params list',
220
);
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
$params_lookup[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params lookup list',
221
)
);
},
$query
);
}
// return, old query is always set
return [
// original
'original' => [
'query' => $query,
'params' => $params,
],
// type found, empty if nothing was done
'type' => $type,
// int|null: found, not found; false: problem
'found' => $found,
'matches' => $matches_return,
// old to new lookup check
'params_lookup' => $params_lookup,
// new
'query' => $query_new ?? '',
'params' => $params_new,
];
}
}
// __END__

View File

@@ -310,7 +310,7 @@ class Generate
* construct form generator
*
* 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_config db config array, mandatory
* @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 config array, mandatory
* @param \CoreLibs\Logging\Logging $log Logging class
* @param \CoreLibs\Language\L10n $l10n l10n language class
* @param array<string,mixed> $login_acl Login ACL array,

View File

@@ -42,6 +42,7 @@ return array(
'CoreLibs\\DB\\Options\\Convert' => $baseDir . '/lib/CoreLibs/DB/Options/Convert.php',
'CoreLibs\\DB\\SQL\\Interface\\SqlFunctions' => $baseDir . '/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php',
'CoreLibs\\DB\\SQL\\PgSQL' => $baseDir . '/lib/CoreLibs/DB/SQL/PgSQL.php',
'CoreLibs\\DB\\Support\\ConvertPlaceholder' => $baseDir . '/lib/CoreLibs/DB/Support/ConvertPlaceholder.php',
'CoreLibs\\Debug\\FileWriter' => $baseDir . '/lib/CoreLibs/Debug/FileWriter.php',
'CoreLibs\\Debug\\Logging' => $baseDir . '/lib/CoreLibs/Debug/Logging.php',
'CoreLibs\\Debug\\LoggingLegacy' => $baseDir . '/lib/CoreLibs/Debug/LoggingLegacy.php',

View File

@@ -70,6 +70,7 @@ class ComposerStaticInit10fe8fe2ec4017b8644d2b64bcf398b9
'CoreLibs\\DB\\Options\\Convert' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/Options/Convert.php',
'CoreLibs\\DB\\SQL\\Interface\\SqlFunctions' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php',
'CoreLibs\\DB\\SQL\\PgSQL' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/SQL/PgSQL.php',
'CoreLibs\\DB\\Support\\ConvertPlaceholder' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/Support/ConvertPlaceholder.php',
'CoreLibs\\Debug\\FileWriter' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/FileWriter.php',
'CoreLibs\\Debug\\Logging' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/Logging.php',
'CoreLibs\\Debug\\LoggingLegacy' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/LoggingLegacy.php',