prefix = $options['prefix'];} if (isset($options['testMode']) && $options['testMode'] == true) {$this->testMode = true;return;} $options['type'] = $options['type'] ?? $options['database_type'];if (!isset($options['pdo'])) {$options['database'] = $options['database'] ?? $options['database_name'];if (!isset($options['socket'])) {$options['host'] = $options['host'] ?? $options['server'] ?? false;} } if (isset($options['type'])) {$this->type = strtolower($options['type']);if ($this->type === 'mariadb') {$this->type = 'mysql';} } if (isset($options['logging']) && is_bool($options['logging'])) {$this->logging = $options['logging'];} $option = $options['option'] ?? [];$commands = [];switch ($this->type) {case 'mysql': $commands[] = 'SET SQL_MODE=ANSI_QUOTES';break;case 'mssql': $commands[] = 'SET QUOTED_IDENTIFIER ON';$commands[] = 'SET ANSI_NULLS ON';break;} if (isset($options['pdo'])) {if (!$options['pdo'] instanceof PDO) {throw new InvalidArgumentException('Invalid PDO object supplied.');} $this->pdo = $options['pdo'];foreach ($commands as $value) {$this->pdo->exec($value);} return;} if (isset($options['dsn'])) {if (is_array($options['dsn']) && isset($options['dsn']['driver'])) {$attr = $options['dsn'];} else {throw new InvalidArgumentException('Invalid DSN option supplied.');} } else {if ( isset($options['port']) && is_int($options['port'] * 1) ) {$port = $options['port'];} $isPort = isset($port);switch ($this->type) {case 'mysql': $attr = [ 'driver' => 'mysql', 'dbname' => $options['database'] ];if (isset($options['socket'])) {$attr['unix_socket'] = $options['socket'];} else {$attr['host'] = $options['host'];if ($isPort) {$attr['port'] = $port;} } break;case 'pgsql': $attr = ['driver' => 'pgsql','host' => $options['host'],'dbname' => $options['database']];if ($isPort) {$attr['port'] = $port;} break;case 'sybase': $attr = ['driver' => 'dblib','host' => $options['host'],'dbname' => $options['database']];if ($isPort) {$attr['port'] = $port;} break;case 'oracle': $attr = [ 'driver' => 'oci', 'dbname' => $options['host'] ? '//' . $options['host'] . ($isPort ? ':' . $port : ':1521') . '/' . $options['database'] : $options['database'] ];if (isset($options['charset'])) {$attr['charset'] = $options['charset'];} break;case 'mssql': if (isset($options['driver']) && $options['driver'] === 'dblib') {$attr = [ 'driver' => 'dblib', 'host' => $options['host'] . ($isPort ? ':' . $port : ''), 'dbname' => $options['database'] ];if (isset($options['appname'])) {$attr['appname'] = $options['appname'];} if (isset($options['charset'])) {$attr['charset'] = $options['charset'];} } else {$attr = [ 'driver' => 'sqlsrv', 'Server' => $options['host'] . ($isPort ? ',' . $port : ''), 'Database' => $options['database'] ];if (isset($options['appname'])) {$attr['APP'] = $options['appname'];} $config = ['ApplicationIntent','AttachDBFileName','Authentication','ColumnEncryption','ConnectionPooling','Encrypt','Failover_Partner','KeyStoreAuthentication','KeyStorePrincipalId','KeyStoreSecret','LoginTimeout','MultipleActiveResultSets','MultiSubnetFailover','Scrollable','TraceFile','TraceOn','TransactionIsolation','TransparentNetworkIPResolution','TrustServerCertificate','WSID',];foreach ($config as $value) {$keyname = strtolower(preg_replace(['/([a-z\d])([A-Z])/','/([^_])([A-Z][a-z])/'],'$1_$2',$value));if (isset($options[$keyname])) {$attr[$value] = $options[$keyname];} } } break;case 'sqlite': $attr = [ 'driver' => 'sqlite', $options['database'] ];break;} } if (!isset($attr)) {throw new InvalidArgumentException('Incorrect connection options.');} $driver = $attr['driver'];if (!in_array($driver,PDO::getAvailableDrivers())) {throw new InvalidArgumentException("Unsupported PDO driver: {$driver}.");} unset($attr['driver']);$stack = [];foreach ($attr as $key => $value) {$stack[] = is_int($key) ? $value : $key . '=' . $value;} $dsn = $driver . ':' . implode(';',$stack);if ( in_array($this->type,['mysql','pgsql','sybase','mssql']) && isset($options['charset']) ) {$commands[] = "SET NAMES '{$options['charset']}'" . ( $this->type === 'mysql' && isset($options['collation']) ? " COLLATE '{$options['collation']}'" : '' );} $this->dsn = $dsn;try {$this->pdo = new PDO( $dsn, $options['username'] ?? null, $options['password'] ?? null, $option );if (isset($options['error'])) {$this->pdo->setAttribute( PDO::ATTR_ERRMODE, in_array($options['error'],[ PDO::ERRMODE_SILENT, PDO::ERRMODE_WARNING, PDO::ERRMODE_EXCEPTION ]) ? $options['error'] : PDO::ERRMODE_SILENT );} if (isset($options['command']) && is_array($options['command'])) {$commands = array_merge($commands,$options['command']);} foreach ($commands as $value) {$this->pdo->exec($value);} } catch (PDOException $e) {throw new PDOException($e->getMessage());} } protected function mapKey(): string {return ':MeD' . $this->guid++ . '_mK';} public function query(string $statement,array $map = []): ?PDOStatement {$raw = $this->raw($statement,$map);$statement = $this->buildRaw($raw,$map);return $this->exec($statement,$map);} public function exec(string $statement,array $map = [],callable $callback = null): ?PDOStatement {$this->statement = null;$this->errorInfo = null;$this->error = null;if ($this->testMode) {$this->queryString = $this->generate($statement,$map);return null;} if ($this->debugMode) {if ($this->debugLogging) {$this->debugLogs[] = $this->generate($statement,$map);return null;} echo $this->generate($statement,$map);$this->debugMode = false;return null;} if ($this->logging) {$this->logs[] = [$statement,$map];} else {$this->logs = [[$statement,$map]];} $statement = $this->pdo->prepare($statement);$errorInfo = $this->pdo->errorInfo();if ($errorInfo[0] !== '00000') {$this->errorInfo = $errorInfo;$this->error = $errorInfo[2];return null;} foreach ($map as $key => $value) {$statement->bindValue($key,$value[0],$value[1]);} if (is_callable($callback)) {$this->pdo->beginTransaction();$callback($statement);$execute = $statement->execute();$this->pdo->commit();} else {$execute = $statement->execute();} $errorInfo = $statement->errorInfo();if ($errorInfo[0] !== '00000') {$this->errorInfo = $errorInfo;$this->error = $errorInfo[2];return null;} if ($execute) {$this->statement = $statement;} return $statement;} protected function generate(string $statement,array $map): string {$identifier = [ 'mysql' => '`$1`', 'mssql' => '[$1]' ];$statement = preg_replace( '/(?!\'[^\s]+\s?)"([\p{L}_][\p{L}\p{N}@$#\-_]*)"(?!\s?[^\s]+\')/u', $identifier[$this->type] ?? '"$1"', $statement );foreach ($map as $key => $value) {if ($value[1] === PDO::PARAM_STR) {$replace = $this->quote("{$value[0]}");} elseif ($value[1] === PDO::PARAM_NULL) {$replace = 'NULL';} elseif ($value[1] === PDO::PARAM_LOB) {$replace = '{LOB_DATA}';} else {$replace = $value[0] . '';} $statement = str_replace($key,$replace,$statement);} return $statement;} public static function raw(string $string,array $map = []): Raw {$raw = new Raw();$raw->map = $map;$raw->value = $string;return $raw;} protected function isRaw($object): bool {return $object instanceof Raw;} protected function buildRaw($raw,array &$map): ?string {if (!$this->isRaw($raw)) {return null;} $query = preg_replace_callback( '/(([`\'])[\<]*?)?((FROM|TABLE|INTO|UPDATE|JOIN|TABLE IF EXISTS)\s*)?\<(([\p{L}_][\p{L}\p{N}@$#\-_]*)(\.[\p{L}_][\p{L}\p{N}@$#\-_]*)?)\>([^,]*?\2)?/', function ($matches) {if (!empty($matches[2]) && isset($matches[8])) {return $matches[0];} if (!empty($matches[4])) {return $matches[1] . $matches[4] . ' ' . $this->tableQuote($matches[5]);} return $matches[1] . $this->columnQuote($matches[5]);}, $raw->value );$rawMap = $raw->map;if (!empty($rawMap)) {foreach ($rawMap as $key => $value) {$map[$key] = $this->typeMap($value,gettype($value));} } return $query;} public function quote(string $string): string {if ($this->type === 'mysql') {return "'" . preg_replace(['/([\'"])/','/(\\\\\\\")/'],["\\\\\${1}",'\\\${1}'],$string) . "'";} return "'" . preg_replace('/\'/','\'\'',$string) . "'";} public function tableQuote(string $table): string {if (preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*$/u',$table)) {return '"' . $this->prefix . $table . '"';} throw new InvalidArgumentException("Incorrect table name: {$table}.");} public function columnQuote(string $column): string {if (preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*(\.?[\p{L}_][\p{L}\p{N}@$#\-_]*)?$/u',$column)) {return strpos($column,'.') !== false ? '"' . $this->prefix . str_replace('.','"."',$column) . '"' : '"' . $column . '"';} throw new InvalidArgumentException("Incorrect column name: {$column}.");} protected function typeMap($value,string $type): array {$map = ['NULL' => PDO::PARAM_NULL,'integer' => PDO::PARAM_INT,'double' => PDO::PARAM_STR,'boolean' => PDO::PARAM_BOOL,'string' => PDO::PARAM_STR,'object' => PDO::PARAM_STR,'resource' => PDO::PARAM_LOB];if ($type === 'boolean') {$value = ($value ? '1' : '0');} elseif ($type === 'NULL') {$value = null;} return [$value,$map[$type]];} protected function columnPush(&$columns,array &$map,bool $root,bool $isJoin = false): string {if ($columns === '*') {return $columns;} $stack = [];$hasDistinct = false;if (is_string($columns)) {$columns = [$columns];} foreach ($columns as $key => $value) {$isIntKey = is_int($key);$isArrayValue = is_array($value);if (!$isIntKey && $isArrayValue && $root && count(array_keys($columns)) === 1) {$stack[] = $this->columnQuote($key);$stack[] = $this->columnPush($value,$map,false,$isJoin);} elseif ($isArrayValue) {$stack[] = $this->columnPush($value,$map,false,$isJoin);} elseif (!$isIntKey && $raw = $this->buildRaw($value,$map)) {preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_\.]*)(\s*\[(?(String|Bool|Int|Number))\])?/u',$key,$match);$stack[] = "{$raw} AS {$this->columnQuote($match['column'])}";} elseif ($isIntKey && is_string($value)) {if ($isJoin && strpos($value,'*') !== false) {throw new InvalidArgumentException('Cannot use table.* to select all columns while joining table.');} preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_\.]*)(?:\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u',$value,$match);$columnString = '';if (!empty($match['alias'])) {$columnString = "{$this->columnQuote($match['column'])} AS {$this->columnQuote($match['alias'])}";$columns[$key] = $match['alias'];if (!empty($match['type'])) {$columns[$key] .= ' [' . $match['type'] . ']';} } else {$columnString = $this->columnQuote($match['column']);} if (!$hasDistinct && strpos($value,'@') === 0) {$columnString = 'DISTINCT ' . $columnString;$hasDistinct = true;array_unshift($stack,$columnString);continue;} $stack[] = $columnString;} } return implode(',',$stack);} protected function dataImplode(array $data,array &$map,string $conjunctor): string {$stack = [];foreach ($data as $key => $value) {$type = gettype($value);if ( $type === 'array' && preg_match("/^(AND|OR)(\s+#.*)?$/",$key,$relationMatch) ) {$stack[] = '(' . $this->dataImplode($value,$map,' ' . $relationMatch[1]) . ')';continue;} $mapKey = $this->mapKey();$isIndex = is_int($key);preg_match( '/([\p{L}_][\p{L}\p{N}@$#\-_\.]*)(\[(?.*)\])?([\p{L}_][\p{L}\p{N}@$#\-_\.]*)?/u', $isIndex ? $value : $key, $match );$column = $this->columnQuote($match[1]);$operator = $match['operator'] ?? null;if ($isIndex && isset($match[4]) && in_array($operator,['>','>=','<','<=','=','!='])) {$stack[] = "{$column} {$operator} " . $this->columnQuote($match[4]);continue;} if ($operator && $operator != '=') {if (in_array($operator,['>','>=','<','<='])) {$condition = "{$column} {$operator} ";if (is_numeric($value)) {$condition .= $mapKey;$map[$mapKey] = [$value,is_float($value) ? PDO::PARAM_STR : PDO::PARAM_INT];} elseif ($raw = $this->buildRaw($value,$map)) {$condition .= $raw;} else {$condition .= $mapKey;$map[$mapKey] = [$value,PDO::PARAM_STR];} $stack[] = $condition;} elseif ($operator === '!') {switch ($type) {case 'NULL': $stack[] = $column . ' IS NOT NULL';break;case 'array': $values = [];foreach ($value as $index => $item) {if ($raw = $this->buildRaw($item,$map)) {$values[] = $raw;} else {$stackKey = $mapKey . $index . '_i';$values[] = $stackKey;$map[$stackKey] = $this->typeMap($item,gettype($item));} } $stack[] = $column . ' NOT IN (' . implode(',',$values) . ')';break;case 'object': if ($raw = $this->buildRaw($value,$map)) {$stack[] = "{$column} != {$raw}";} break;case 'integer': case 'double': case 'boolean': case 'string': $stack[] = "{$column} != {$mapKey}";$map[$mapKey] = $this->typeMap($value,$type);break;} } elseif ($operator === '~' || $operator === '!~') {if ($type !== 'array') {$value = [$value];} $connector = ' OR ';$data = array_values($value);if (is_array($data[0])) {if (isset($value['AND']) || isset($value['OR'])) {$connector = ' ' . array_keys($value)[0] . ' ';$value = $data[0];} } $likeClauses = [];foreach ($value as $index => $item) {$likeKey = "{$mapKey}_{$index}_i";$item = strval($item);if (!preg_match('/((?' || $operator === '><') {if ($type === 'array') {if ($operator === '><') {$column .= ' NOT';} if ($this->isRaw($value[0]) && $this->isRaw($value[1])) {$stack[] = "({$column} BETWEEN {$this->buildRaw($value[0],$map)} AND {$this->buildRaw($value[1],$map)})";} else {$stack[] = "({$column} BETWEEN {$mapKey}a AND {$mapKey}b)";$dataType = (is_numeric($value[0]) && is_numeric($value[1])) ? PDO::PARAM_INT : PDO::PARAM_STR;$map[$mapKey . 'a'] = [$value[0],$dataType];$map[$mapKey . 'b'] = [$value[1],$dataType];} } } elseif ($operator === 'REGEXP') {$stack[] = "{$column} REGEXP {$mapKey}";$map[$mapKey] = [$value,PDO::PARAM_STR];} else {throw new InvalidArgumentException("Invalid operator [{$operator}] for column {$column} supplied.");} continue;} switch ($type) {case 'NULL': $stack[] = $column . ' IS NULL';break;case 'array': $values = [];foreach ($value as $index => $item) {if ($raw = $this->buildRaw($item,$map)) {$values[] = $raw;} else {$stackKey = $mapKey . $index . '_i';$values[] = $stackKey;$map[$stackKey] = $this->typeMap($item,gettype($item));} } $stack[] = $column . ' IN (' . implode(',',$values) . ')';break;case 'object': if ($raw = $this->buildRaw($value,$map)) {$stack[] = "{$column} = {$raw}";} break;case 'integer': case 'double': case 'boolean': case 'string': $stack[] = "{$column} = {$mapKey}";$map[$mapKey] = $this->typeMap($value,$type);break;} } return implode($conjunctor . ' ',$stack);} protected function whereClause($where,array &$map): string {$clause = '';if (is_array($where)) {$conditions = array_diff_key($where,array_flip( ['GROUP','ORDER','HAVING','LIMIT','LIKE','MATCH'] ));if (!empty($conditions)) {$clause = ' WHERE ' . $this->dataImplode($conditions,$map,' AND');} if (isset($where['MATCH']) && $this->type === 'mysql') {$match = $where['MATCH'];if (is_array($match) && isset($match['columns'],$match['keyword'])) {$mode = '';$options = ['natural' => 'IN NATURAL LANGUAGE MODE','natural+query' => 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION','boolean' => 'IN BOOLEAN MODE','query' => 'WITH QUERY EXPANSION'];if (isset($match['mode'],$options[$match['mode']])) {$mode = ' ' . $options[$match['mode']];} $columns = implode(',',array_map([$this,'columnQuote'],$match['columns']));$mapKey = $this->mapKey();$map[$mapKey] = [$match['keyword'],PDO::PARAM_STR];$clause .= ($clause !== '' ? ' AND ' : ' WHERE') . ' MATCH (' . $columns . ') AGAINST (' . $mapKey . $mode . ')';} } if (isset($where['GROUP'])) {$group = $where['GROUP'];if (is_array($group)) {$stack = [];foreach ($group as $column => $value) {$stack[] = $this->columnQuote($value);} $clause .= ' GROUP BY ' . implode(',',$stack);} elseif ($raw = $this->buildRaw($group,$map)) {$clause .= ' GROUP BY ' . $raw;} else {$clause .= ' GROUP BY ' . $this->columnQuote($group);} } if (isset($where['HAVING'])) {$having = $where['HAVING'];if ($raw = $this->buildRaw($having,$map)) {$clause .= ' HAVING ' . $raw;} else {$clause .= ' HAVING ' . $this->dataImplode($having,$map,' AND');} } if (isset($where['ORDER'])) {$order = $where['ORDER'];if (is_array($order)) {$stack = [];foreach ($order as $column => $value) {if (is_array($value)) {$valueStack = [];foreach ($value as $item) {$valueStack[] = is_int($item) ? $item : $this->quote($item);} $valueString = implode(',',$valueStack);$stack[] = "FIELD({$this->columnQuote($column)},{$valueString})";} elseif ($value === 'ASC' || $value === 'DESC') {$stack[] = $this->columnQuote($column) . ' ' . $value;} elseif (is_int($column)) {$stack[] = $this->columnQuote($value);} } $clause .= ' ORDER BY ' . implode(',',$stack);} elseif ($raw = $this->buildRaw($order,$map)) {$clause .= ' ORDER BY ' . $raw;} else {$clause .= ' ORDER BY ' . $this->columnQuote($order);} } if (isset($where['LIMIT'])) {$limit = $where['LIMIT'];if (in_array($this->type,['oracle','mssql'])) {if ($this->type === 'mssql' && !isset($where['ORDER'])) {$clause .= ' ORDER BY (SELECT 0)';} if (is_numeric($limit)) {$limit = [0,$limit];} if ( is_array($limit) && is_numeric($limit[0]) && is_numeric($limit[1]) ) {$clause .= " OFFSET {$limit[0]} ROWS FETCH NEXT {$limit[1]} ROWS ONLY";} } else {if (is_numeric($limit)) {$clause .= ' LIMIT ' . $limit;} elseif ( is_array($limit) && is_numeric($limit[0]) && is_numeric($limit[1]) ) {$clause .= " LIMIT {$limit[1]} OFFSET {$limit[0]}";}}}} elseif ($raw = $this->buildRaw($where,$map)) {$clause .= ' ' . $raw;} return $clause;} protected function selectContext( string $table, array &$map, $join, &$columns = null, $where = null, $columnFn = null ): string {preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_]*)\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\)/u',$table,$tableMatch);if (isset($tableMatch['table'],$tableMatch['alias'])) {$table = $this->tableQuote($tableMatch['table']);$tableAlias = $this->tableQuote($tableMatch['alias']);$tableQuery = "{$table} AS {$tableAlias}";} else {$table = $this->tableQuote($table);$tableQuery = $table;} $isJoin = $this->isJoin($join);if ($isJoin) {$tableQuery .= ' ' . $this->buildJoin($tableAlias ?? $table,$join,$map);} else {if (is_null($columns)) {if ( !is_null($where) || (is_array($join) && isset($columnFn)) ) {$where = $join;$columns = null;} else {$where = null;$columns = $join;} } else {$where = $columns;$columns = $join;} } if (isset($columnFn)) {if ($columnFn === 1) {$column = '1';if (is_null($where)) {$where = $columns;} } elseif ($raw = $this->buildRaw($columnFn,$map)) {$column = $raw;} else {if (empty($columns) || $this->isRaw($columns)) {$columns = '*';$where = $join;} $column = $columnFn . '(' . $this->columnPush($columns,$map,true) . ')';} } else {$column = $this->columnPush($columns,$map,true,$isJoin);} return 'SELECT ' . $column . ' FROM ' . $tableQuery . $this->whereClause($where,$map);} protected function isJoin($join): bool {if (!is_array($join)) {return false;} $keys = array_keys($join);if ( isset($keys[0]) && is_string($keys[0]) && strpos($keys[0],'[') === 0 ) {return true;} return false;} protected function buildJoin(string $table,array $join,array &$map): string {$tableJoin = [];$type = ['>' => 'LEFT','<' => 'RIGHT','<>' => 'FULL','><' => 'INNER'];foreach ($join as $subtable => $relation) {preg_match('/(\[(?\<\>?|\>\[\p{L}_][\p{L}\p{N}@$#\-_]*)\s?(\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?/u',$subtable,$match);if ($match['join'] === '' || $match['table'] === '') {continue;} if (is_string($relation)) {$relation = 'USING ("' . $relation . '")';} elseif (is_array($relation)) {if (isset($relation[0])) {$relation = 'USING ("' . implode('","',$relation) . '")';} else {$joins = [];foreach ($relation as $key => $value) {if ($key === 'AND' && is_array($value)) {$joins[] = $this->dataImplode($value,$map,' AND');continue;} $joins[] = ( strpos($key,'.') > 0 ? $this->columnQuote($key) : $table . '.' . $this->columnQuote($key) ) . ' = ' . $this->tableQuote($match['alias'] ?? $match['table']) . '.' . $this->columnQuote($value);} $relation = 'ON ' . implode(' AND ',$joins);} } elseif ($raw = $this->buildRaw($relation,$map)) {$relation = $raw;} $tableName = $this->tableQuote($match['table']);if (isset($match['alias'])) {$tableName .= ' AS ' . $this->tableQuote($match['alias']);} $tableJoin[] = $type[$match['join']] . " JOIN {$tableName} {$relation}";} return implode(' ',$tableJoin);} protected function columnMap($columns,array &$stack,bool $root): array {if ($columns === '*') {return $stack;} foreach ($columns as $key => $value) {if (is_int($key)) {preg_match('/([\p{L}_][\p{L}\p{N}@$#\-_]*\.)?(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(?:\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u',$value,$keyMatch);$columnKey = !empty($keyMatch['alias']) ? $keyMatch['alias'] : $keyMatch['column'];$stack[$value] = isset($keyMatch['type']) ? [$columnKey,$keyMatch['type']] : [$columnKey];} elseif ($this->isRaw($value)) {preg_match('/([\p{L}_][\p{L}\p{N}@$#\-_]*\.)?(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(\s*\[(?(String|Bool|Int|Number))\])?/u',$key,$keyMatch);$columnKey = $keyMatch['column'];$stack[$key] = isset($keyMatch['type']) ? [$columnKey,$keyMatch['type']] : [$columnKey];} elseif (!is_int($key) && is_array($value)) {if ($root && count(array_keys($columns)) === 1) {$stack[$key] = [$key,'String'];} $this->columnMap($value,$stack,false);} } return $stack;} protected function dataMap( array $data, array $columns, array $columnMap, array &$stack, bool $root, array &$result = null ): void {if ($root) {$columnsKey = array_keys($columns);if (count($columnsKey) === 1 && is_array($columns[$columnsKey[0]])) {$indexKey = array_keys($columns)[0];$dataKey = preg_replace("/^[\p{L}_][\p{L}\p{N}@$#\-_]*\./u",'',$indexKey);$currentStack = [];foreach ($data as $item) {$this->dataMap($data,$columns[$indexKey],$columnMap,$currentStack,false,$result);$index = $data[$dataKey];if (isset($result)) {$result[$index] = $currentStack;} else {$stack[$index] = $currentStack;} } } else {$currentStack = [];$this->dataMap($data,$columns,$columnMap,$currentStack,false,$result);if (isset($result)) {$result[] = $currentStack;} else {$stack = $currentStack;} } return;} foreach ($columns as $key => $value) {$isRaw = $this->isRaw($value);if (is_int($key) || $isRaw) {$map = $columnMap[$isRaw ? $key : $value];$columnKey = $map[0];$item = $data[$columnKey];if (isset($map[1])) {if ($isRaw && in_array($map[1],['Object','JSON'])) {continue;} if (is_null($item)) {$stack[$columnKey] = null;continue;} switch ($map[1]) {case 'Number': $stack[$columnKey] = (float) $item;break;case 'Int': $stack[$columnKey] = (int) $item;break;case 'Bool': $stack[$columnKey] = (bool) $item;break;case 'Object': $stack[$columnKey] = unserialize($item);break;case 'JSON': $stack[$columnKey] = json_decode($item,true);break;case 'String': $stack[$columnKey] = (string) $item;break;} } else {$stack[$columnKey] = $item;}} else {$currentStack = [];$this->dataMap($data,$value,$columnMap,$currentStack,false,$result);$stack[$key] = $currentStack;}}} private function returningQuery($query,&$map,&$data): ?PDOStatement {$returnColumns = array_map( function ($value) {return $value[0];}, $data );$query .= ' RETURNING ' . implode(',',array_map([$this,'columnQuote'],$returnColumns)) . ' INTO ' . implode(',',array_keys($data));return $this->exec($query,$map,function ($statement) use (&$data) {foreach ($data as $key => $return) {if (isset($return[3])) {$statement->bindParam($key,$data[$key][1],$return[2],$return[3]);} else {$statement->bindParam($key,$data[$key][1],$return[2]);} } });} public function create(string $table,$columns,$options = null): ?PDOStatement {$stack = [];$tableOption = '';$tableName = $this->tableQuote($table);foreach ($columns as $name => $definition) {if (is_int($name)) {$stack[] = preg_replace('/\<([\p{L}_][\p{L}\p{N}@$#\-_]*)\>/u','"$1"',$definition);} elseif (is_array($definition)) {$stack[] = $this->columnQuote($name) . ' ' . implode(' ',$definition);} elseif (is_string($definition)) {$stack[] = $this->columnQuote($name) . ' ' . $definition;} } if (is_array($options)) {$optionStack = [];foreach ($options as $key => $value) {if (is_string($value) || is_int($value)) {$optionStack[] = "{$key} = {$value}";} } $tableOption = ' ' . implode(',',$optionStack);} elseif (is_string($options)) {$tableOption = ' ' . $options;} $command = 'CREATE TABLE';if (in_array($this->type,['mysql','pgsql','sqlite'])) {$command .= ' IF NOT EXISTS';} return $this->exec("{$command} {$tableName} (" . implode(',',$stack) . "){$tableOption}");} public function drop(string $table): ?PDOStatement {return $this->exec('DROP TABLE IF EXISTS ' . $this->tableQuote($table));} public function select(string $table,$join,$columns = null,$where = null): ?array {$map = [];$result = [];$columnMap = [];$args = func_get_args();$lastArgs = $args[array_key_last($args)];$callback = is_callable($lastArgs) ? $lastArgs : null;$where = is_callable($where) ? null : $where;$columns = is_callable($columns) ? null : $columns;$column = $where === null ? $join : $columns;$isSingle = (is_string($column) && $column !== '*');$statement = $this->exec($this->selectContext($table,$map,$join,$columns,$where),$map);$this->columnMap($columns,$columnMap,true);if (!$this->statement) {return $result;} if ($columns === '*') {if (isset($callback)) {while ($data = $statement->fetch(PDO::FETCH_ASSOC)) {$callback($data);} return null;} return $statement->fetchAll(PDO::FETCH_ASSOC);} while ($data = $statement->fetch(PDO::FETCH_ASSOC)) {$currentStack = [];if (isset($callback)) {$this->dataMap($data,$columns,$columnMap,$currentStack,true);$callback( $isSingle ? $currentStack[$columnMap[$column][0]] : $currentStack );} else {$this->dataMap($data,$columns,$columnMap,$currentStack,true,$result);} } if (isset($callback)) {return null;} if ($isSingle) {$singleResult = [];$resultKey = $columnMap[$column][0];foreach ($result as $item) {$singleResult[] = $item[$resultKey];} return $singleResult;} return $result;} public function insert(string $table,array $values,string $primaryKey = null): ?PDOStatement {$stack = [];$columns = [];$fields = [];$map = [];$returnings = [];if (!isset($values[0])) {$values = [$values];} foreach ($values as $data) {foreach ($data as $key => $value) {$columns[] = $key;} } $columns = array_unique($columns);foreach ($values as $data) {$values = [];foreach ($columns as $key) {$value = $data[$key];$type = gettype($value);if ($this->type === 'oracle' && $type === 'resource') {$values[] = 'EMPTY_BLOB()';$returnings[$this->mapKey()] = [$key,$value,PDO::PARAM_LOB];continue;} if ($raw = $this->buildRaw($data[$key],$map)) {$values[] = $raw;continue;} $mapKey = $this->mapKey();$values[] = $mapKey;switch ($type) {case 'array': $map[$mapKey] = [ strpos($key,'[JSON]') === strlen($key) - 6 ? json_encode($value) : serialize($value), PDO::PARAM_STR ];break;case 'object': $value = serialize($value);break;case 'NULL':case 'resource':case 'boolean':case 'integer':case 'double':case 'string': $map[$mapKey] = $this->typeMap($value,$type);break;} } $stack[] = '(' . implode(',',$values) . ')';} foreach ($columns as $key) {$fields[] = $this->columnQuote(preg_replace("/(\s*\[JSON\]$)/i",'',$key));} $query = 'INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(',',$fields) . ') VALUES ' . implode(',',$stack);if ( $this->type === 'oracle' && (!empty($returnings) || isset($primaryKey)) ) {if ($primaryKey) {$returnings[':RETURNID'] = [$primaryKey,'',PDO::PARAM_INT,8];} $statement = $this->returningQuery($query,$map,$returnings);if ($primaryKey) {$this->returnId = $returnings[':RETURNID'][1];} return $statement;} return $this->exec($query,$map);} public function update(string $table,$data,$where = null): ?PDOStatement {$fields = [];$map = [];$returnings = [];foreach ($data as $key => $value) {$column = $this->columnQuote(preg_replace("/(\s*\[(JSON|\+|\-|\*|\/)\]$)/",'',$key));$type = gettype($value);if ($this->type === 'oracle' && $type === 'resource') {$fields[] = "{$column} = EMPTY_BLOB()";$returnings[$this->mapKey()] = [$key,$value,PDO::PARAM_LOB];continue;} if ($raw = $this->buildRaw($value,$map)) {$fields[] = "{$column} = {$raw}";continue;} preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(\[(?\+|\-|\*|\/)\])?/u',$key,$match);if (isset($match['operator'])) {if (is_numeric($value)) {$fields[] = "{$column} = {$column} {$match['operator']} {$value}";} } else {$mapKey = $this->mapKey();$fields[] = "{$column} = {$mapKey}";switch ($type) {case 'array': $map[$mapKey] = [ strpos($key,'[JSON]') === strlen($key) - 6 ? json_encode($value) : serialize($value), PDO::PARAM_STR ];break;case 'object': $value = serialize($value);break;case 'NULL':case 'resource':case 'boolean':case 'integer':case 'double':case 'string': $map[$mapKey] = $this->typeMap($value,$type);break;}}} $query = 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(',',$fields) . $this->whereClause($where,$map);if ($this->type === 'oracle' && !empty($returnings)) {return $this->returningQuery($query,$map,$returnings);} return $this->exec($query,$map);} public function delete(string $table,$where): ?PDOStatement {$map = [];return $this->exec('DELETE FROM ' . $this->tableQuote($table) . $this->whereClause($where,$map),$map);} public function replace(string $table,array $columns,$where = null): ?PDOStatement {$map = [];$stack = [];foreach ($columns as $column => $replacements) {if (is_array($replacements)) {foreach ($replacements as $old => $new) {$mapKey = $this->mapKey();$columnName = $this->columnQuote($column);$stack[] = "{$columnName} = REPLACE({$columnName},{$mapKey}a,{$mapKey}b)";$map[$mapKey . 'a'] = [$old,PDO::PARAM_STR];$map[$mapKey . 'b'] = [$new,PDO::PARAM_STR];} } } if (empty($stack)) {throw new InvalidArgumentException('Invalid columns supplied.');} return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(',',$stack) . $this->whereClause($where,$map),$map);} public function get(string $table,$join = null,$columns = null,$where = null) {$map = [];$result = [];$columnMap = [];$currentStack = [];if ($where === null) {if ($this->isJoin($join)) {$where['LIMIT'] = 1;} else {$columns['LIMIT'] = 1;} $column = $join;} else {$column = $columns;$where['LIMIT'] = 1;} $isSingle = (is_string($column) && $column !== '*');$query = $this->exec($this->selectContext($table,$map,$join,$columns,$where),$map);if (!$this->statement) {return false;} $data = $query->fetchAll(PDO::FETCH_ASSOC);if (isset($data[0])) {if ($column === '*') {return $data[0];} $this->columnMap($columns,$columnMap,true);$this->dataMap($data[0],$columns,$columnMap,$currentStack,true,$result);if ($isSingle) {return $result[0][$columnMap[$column][0]];} return $result[0];} } public function has(string $table,$join,$where = null): bool {$map = [];$column = null;$query = $this->exec( $this->type === 'mssql' ? $this->selectContext($table,$map,$join,$column,$where,Medoo::raw('TOP 1 1')) : 'SELECT EXISTS(' . $this->selectContext($table,$map,$join,$column,$where,1) . ')', $map );if (!$this->statement) {return false;} $result = $query->fetchColumn();return $result === '1' || $result === 1 || $result === true;} public function rand(string $table,$join = null,$columns = null,$where = null): array {$orderRaw = $this->raw( $this->type === 'mysql' ? 'RAND()' : ($this->type === 'mssql' ? 'NEWID()' : 'RANDOM()') );if ($where === null) {if ($this->isJoin($join)) {$where['ORDER'] = $orderRaw;} else {$columns['ORDER'] = $orderRaw;} } else {$where['ORDER'] = $orderRaw;} return $this->select($table,$join,$columns,$where);} private function aggregate(string $type,string $table,$join = null,$column = null,$where = null): ?string {$map = [];$query = $this->exec($this->selectContext($table,$map,$join,$column,$where,$type),$map);if (!$this->statement) {return null;} return (string) $query->fetchColumn();} public function count(string $table,$join = null,$column = null,$where = null): ?int {return (int) $this->aggregate('COUNT',$table,$join,$column,$where);} public function avg(string $table,$join,$column = null,$where = null): ?string {return $this->aggregate('AVG',$table,$join,$column,$where);} public function max(string $table,$join,$column = null,$where = null): ?string {return $this->aggregate('MAX',$table,$join,$column,$where);} public function min(string $table,$join,$column = null,$where = null): ?string {return $this->aggregate('MIN',$table,$join,$column,$where);} public function sum(string $table,$join,$column = null,$where = null): ?string {return $this->aggregate('SUM',$table,$join,$column,$where);} public function action(callable $actions): void {if (is_callable($actions)) {$this->pdo->beginTransaction();try {$result = $actions($this);if ($result === false) {$this->pdo->rollBack();} else {$this->pdo->commit();} } catch (Exception $e) {$this->pdo->rollBack();throw $e;}}} public function id(string $name = null): ?string {$type = $this->type;if ($type === 'oracle') {return $this->returnId;} elseif ($type === 'pgsql') {$id = $this->pdo->query('SELECT LASTVAL()')->fetchColumn();return (string) $id ?: null;} return $this->pdo->lastInsertId($name);} public function debug(): self {$this->debugMode = true;return $this;} public function beginDebug(): void {$this->debugMode = true;$this->debugLogging = true;} public function debugLog(): array {$this->debugMode = false;$this->debugLogging = false;return $this->debugLogs;} public function last(): ?string {if (empty($this->logs)) {return null;} $log = $this->logs[array_key_last($this->logs)];return $this->generate($log[0],$log[1]);} public function log(): array {return array_map( function ($log) {return $this->generate($log[0],$log[1]);}, $this->logs );} public function info(): array {$output = ['server' => 'SERVER_INFO','driver' => 'DRIVER_NAME','client' => 'CLIENT_VERSION','version' => 'SERVER_VERSION','connection' => 'CONNECTION_STATUS'];foreach ($output as $key => $value) {try {$output[$key] = $this->pdo->getAttribute(constant('PDO::ATTR_' . $value));} catch (PDOException $e) {$output[$key] = $e->getMessage();}} $output['dsn'] = $this->dsn;return $output;}}