59 public function createPdo($options =
null,$attributes =
null){
60 $connection = new \Rsi\Wrapper\Record($this->_connection);
61 $pdo = new \PDO($connection->dsn,$connection->username,$connection->password,array_replace(
62 $this->_defaultOptions,
63 $connection->options ? $connection->options->data : [],
66 foreach(array_replace(
67 $this->_defaultAttributes,
70 [\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
71 ) as $attribute => $value) $pdo->setAttribute($attribute,$value);
72 $this->
component(
'event')->trigger(self::EVENT_OPEN,$this,$pdo);
80 public function date($time =
null){
81 return date(
'Y-m-d',$time ?: time());
89 return date(
'Y-m-d H:i:s',$time ?: time());
98 foreach([$escape,
'%',
'_'] as $char) $value = str_replace($char,$escape . $char,$value);
107 return !strcasecmp(substr(trim($sql),0,7),
'select ');
121 $this->pdo->beginTransaction();
127 $this->pdo->rollBack();
133 $this->pdo->commit();
144 call_user_func($callback,$this);
159 return $this->pdo->lastInsertId();
163 $this->_startTime = microtime(
true);
170 if(($time >= $this->statTime) && ($stats = $this->
component(
'stats')))
171 $stats->inc($this->statPrefix .
':' . trim(preg_replace(
'/\\s+/',
' ',$sql)),$time);
173 foreach($this->logTimes as $prio => $edge)
if($time >= $edge){
174 $log->add($prio,
'Slow query',compact(
'sql',
'args') + [
175 'time' => round($time,
max(0,$this->logTimeSignificant - ceil(log($time,10)))),
176 'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
180 if($this->_fred->debug){
181 if(!array_key_exists($hash = md5($sql . serialize($args)),$this->_duploCount)) $this->_duploCount[$hash] = 0;
182 else $log->info(
'Duplicate query' . ($this->_duploCount[$hash]++ ?
' (' . $this->_duploCount[$hash] .
')' :
''),__FILE__,__LINE__,compact(
'sql',
'args'));
185 $this->allowInsert =
true;
189 if(!$args) $args = [];
190 else foreach($args as $key => $value)
191 if(!preg_match($pattern =
"/:$key\\b/",$sql)) unset($args[$key]);
192 elseif(is_array($value)){
193 if(!$value)
throw new \Exception(
"Empty array for '$key'");
196 foreach($value as $sub) $args[$keys[] = $key .
's' .
count($keys)] = $sub;
197 $sql = preg_replace($pattern,
':' . implode(
',:',$keys),$sql);
207 public function statement($sql,$args =
null,$pdo =
null){
208 if(!$pdo) $pdo = $this->pdo;
209 if($statement = $pdo->prepare($sql)){
210 if($args)
foreach($args as $key => $value){
211 $statement->bindValue(
':' . $key,$value);
214 $statement->execute();
225 public function execute($sql,$args =
null,$pdo =
null){
226 if($log = $this->
component(
'log')) $log->debug(__CLASS__ .
"::execute('$sql',args)",__FILE__,__LINE__,compact(
'sql',
'args'));
227 if(!$pdo) $pdo = $this->pdo;
231 if(!$args) $result = $pdo->exec($sql);
232 elseif($statement = $this->
statement($sql,$args,$pdo)){
233 $result = $statement->rowCount();
234 $statement->closeCursor();
247 public function query($sql,$args =
null,$pdo =
null){
248 if($log = $this->
component(
'log')) $log->debug(__CLASS__ .
"::query('$sql',args)",__FILE__,__LINE__,compact(
'sql',
'args'));
249 if(!$pdo) $pdo = $this->pdo;
252 $result = $args ? $this->
statement($sql,$args,$pdo) : $pdo->query($sql);
262 $row = $statement->fetch();
263 return $this->defTables ? $this->
component(
'def')->convertRecord($row,$this->defTables) : $row;
271 public function all($sql,$args =
null){
272 if($statement = $this->
query($sql,$args)){
273 if(!$this->defTables)
return $statement->fetchAll();
275 while($row = $this->
fetch($statement)) $rows[] = $row;
276 $statement->closeCursor();
290 public function single($sql,$args =
null,$auto =
true){
291 if($statement = $this->
query($sql,$args)){
292 $row = $this->
fetch($statement);
293 $statement->closeCursor();
295 if($row)
return $auto && (
count($row) == 1) ? array_pop($row) : $row;
308 public function record($sql,$args =
null){
310 if($statement = $this->
query($sql,$args)){
311 while($row = $this->
fetch($statement)){
312 $key = array_shift($row);
314 case 0: $result[] = $key;
break;
315 case 1: $result[$key] = array_pop($row);
break;
316 default: $result[$key] = $row;
319 $statement->closeCursor();
334 public function each($callback,$sql,$args =
null){
336 if($statement = $this->
query($sql,$args,$pdo = $this->
createPdo($this->_eachOptions,$this->_eachAttributes))){
338 while($row = $this->
fetch($statement))
if(call_user_func($callback,$row,$result++) ===
false)
break;
339 $statement->closeCursor();
357 public function batch($callback,$sql,$args =
null,$limit = 1000){
358 if(!stripos($sql,
'order by'))
throw new \Exception(
'No ordering');
359 $result = $offset = 0;
360 while(($result == $offset) && ($rows = $this->
all($this->
limit($sql,$limit,$offset),$args))){
361 foreach($rows as $row)
if(call_user_func($callback,$row,$result++) ===
false)
break 2;
372 $keys = $count =
false;
373 foreach($columns as $column => $value)
if(is_array($value)){
374 if($keys ===
false) $count =
count($keys = array_keys($value));
375 elseif(($count !=
count($value)) || array_diff($keys,array_keys($value)))
376 throw new \Exception(
"Different keys for column '$column'");
379 foreach((array)$keys as $key){
381 foreach($columns as $column => $value) $result[$key][$column] = is_array($value) ? $value[$key] : $value;
401 if($extra = $args && is_string($args) ?
"\n" . $args :
null) $args =
null;
402 if(is_array($where)){
403 if(!is_array($args)) $args = [];
404 $def = $this->defTables ? $this->
component(
'def') :
null;
405 foreach($where as $column => &$value){
406 if($raw = $column[0] ==
'!') $column = substr($column,1);
408 if($operator = preg_match(
'/\\W+$/',$column,$match) ? $match[0] :
null){
409 $column = substr($column,0,-strlen($operator));
410 if($operator[0] ==
'!'){
412 $operator = substr($operator,1);
414 switch($short = $operator){
415 case '~' : $operator =
'like';
break;
418 else $short = $operator =
'=';
420 if($value ===
null)
switch($operator){
421 case '<>': $negation =
'not ';
422 case '=': $value =
"`$column` is {$negation}null";
break;
423 default: $value =
'0=1';
425 elseif(is_array($value)){
426 if(array_key_exists($operator,$this->multiOperators)){
427 $multi =
count($value) > 1;
428 $this->
prepareWhere($value,$args,$this->multiOperators[$operator]);
429 if($multi) $value =
"($value)";
432 if($def)
foreach($value as &$sub) $sub = $def->formatColumn($column,$sub,$this->defTables);
434 if(!$negation && (
count($value) == 1)){
435 $args[$key =
'a' .
count($args)] = array_pop($value);
436 $value =
"`$column` $operator :$key";
443 $args[$key =
'a' .
count($args)] = $value;
444 $value =
"`$column` {$negation}in (:$key)";
448 foreach($value as $sub){
449 $args[$key =
'l' .
count($args)] = $sub;
450 $likes[] =
"`$column` like :$key";
452 $value = $negation .
'(' . implode(
' or ',$likes) .
')';
455 $key =
'b' .
count($args);
456 $args += [
'l' . $key => array_shift($value),
'u' . $key => array_shift($value)];
457 if($value)
throw new \InvalidArgumentException(
"Too many arguments for 'between' for column '$column'");
458 $value =
"$negation`$column` between :l$key and :u$key";
461 throw new \DomainException(
"Invalid array operator '$operator' for column '$column'");
465 else $value = (int)($operator ==
'<>') .
'=1';
468 if($null = substr($operator,0,1) ==
'#') $operator = substr($operator,1);
469 if($raw) $value =
"$negation`$column` $operator $value";
471 $args[$key =
'v' .
count($args)] = $def ? $def->formatColumn($column,$value,$this->defTables) : $value;
472 $value =
"$negation`$column` $operator :$key";
474 if($null) $value =
"(`$column` is null or $value)";
478 $where = implode(
"\n $glue ",$where);
480 elseif(!$args) $args = [];
481 $where = ($where ?:
'1=1') . $extra;
490 public function limit($sql,$limit,$offset =
null){
491 return $sql .
' limit ' .
max(0,(
int)$offset) .
',' .
max(0,(
int)$limit);
501 protected function logChange($table,$action,$key,$old,$new){
502 $this->_fred->event->trigger(self::EVENT_LOG_CHANGE,$this,$table,$action,$key,$old,$new);
509 $this->_fred->event->trigger(self::EVENT_LOG_ADD_ID,$this,$this->_logAddId,$id);
519 protected function logChanges($table,$action,$columns,$where,$args =
null){
520 $this->_logAddId =
false;
521 if($key_columns = $this->logTables[$table] ??
null)
try{
522 $key = is_array($where) ? \Rsi\Record::select($where,$key_columns) : [];
523 if($keys =
count($key) ==
count($key_columns) ? [$key] : $this->
select($table,$key_columns,$where,$args)){
524 foreach($keys as $key)
if(
525 ($current = $this->
select($table,$columns ===
false ?
'*' : array_keys($columns),$key,
null,1)) &&
526 ($changed = array_diff_assoc($current,$columns ===
false ? $key : $columns))
527 ) $this->
logChange($table,$action,$key,$changed,$columns ===
false ?
false : array_intersect_key($columns,$changed));
529 elseif($action != self::ACTION_DELETE){
530 $this->
logChange($table,$action,
false,
false,$columns);
531 if(
count($key_columns) == 1) $this->_logAddId = array_pop($key_columns);
535 if($this->_fred->debug)
throw $e;
536 $this->
component(
'log')->critical(
"Could not log '$action' on '$table': " . $e->getMessage(),__FILE__,__LINE__,compact(
'columns',
'where',
'args'));
547 public function scout($table,$columns =
null,$where =
null,$args =
null){
550 if(!array_key_exists($table,$this->_scout)) $this->_scout[$table] = [];
551 if(!array_key_exists($key = serialize($columns),$this->_scout[$table])) $this->_scout[$table][$key] = [];
552 foreach(($result = $this->
select($table,$columns,$where,$args)) as $record) $this->_scout[$table][$key][serialize($record)] = $record;
554 else unset($this->_scout[$table]);
559 if(!is_array($columns))
return $columns;
560 $def = $this->defTables ? $this->
component(
'def') :
null;
562 foreach($columns as $column => $value){
563 if($raw = substr($column,0,1) ==
'!') $column = substr($column,1);
564 elseif($def) $value = $def->formatColumn($column,$value,$this->defTables);
565 $args[$key =
'u' .
count($args)] = $value;
566 $sql[] =
"`$column` = " . ($raw ? $value :
':' . $key);
568 return implode(
',',$sql);
578 public function insert($table,$columns,$action = self::ACTION_INSERT,$update =
null){
579 $this->
scout($table);
580 $def = $this->defTables ? $this->
component(
'def') :
null;
584 foreach($columns as $column => &$value){
585 if($raw = substr($column,0,1) ==
'!') $column = substr($column,1);
586 elseif($def) $value = $def->formatColumn($column,$value,$this->defTables);
587 $values[$column] = $raw ? $value :
':' . $column;
590 $this->
logChanges($table,$action,$update ?: $columns,$args = $columns);
591 $sql =
" into `$table` (`" . implode(
'`,`',array_keys($values)) .
"`)\nvalues (" . implode(
',',$values) .
')';
597 default:
throw new \Exception(
"Unknown insert action '$action'");
599 if($this->
execute($sql,$args)){
601 if($this->_logAddId && ($id = $this->
lastInsertId()))
try{
605 if($this->_fred->debug)
throw $e;
619 return $this->
insert($table,$columns,self::ACTION_CREATE);
628 return $this->
insert($table,$columns,self::ACTION_REPLACE);
636 public function upsert($table,$columns,$update){
637 return $this->
insert($table,$columns,self::ACTION_UPSERT,$update);
650 public function select($table,$columns =
'*',$where =
null,$args =
null,$limit =
null,$offset =
null){
652 ($limit == 1) && !$offset &&
653 array_key_exists($table,$this->_scout) && array_key_exists($key = serialize($columns),$this->_scout[$table]) &&
654 is_array($where) && !array_filter($where,
function($value){
655 return is_array($value);
657 )
foreach($this->_scout[$table][$key] as $record){
658 foreach($where as $key => $value)
if(($record[$key] ??
false) !== $value)
continue 2;
661 $columns = preg_replace(
'/^(`)?\\+/',
'$1',is_array($columns) ?
'`' . implode(
'`,`',$columns) .
'`' : $columns,1,$record);
663 $sql =
"select $columns\nfrom `$table`\nwhere $where";
664 if($limit) $sql = $this->
limit($sql,$limit,$offset);
665 if($record)
return $this->
record($sql,$args);
666 $rows = $this->
all($sql,$args);
667 return $limit ===
true
668 ? (($row = array_shift($rows)) ? array_shift($row) :
false)
669 : ($rows ? ($limit == 1 ? array_shift($rows) : $rows) :
false);
680 public function update($table,$columns,$where,$args =
null,$insert_if_not_exists =
false){
681 $this->
scout($table);
682 $this->
logChanges($table,self::ACTION_UPDATE,$columns,$where,$args);
684 $sql =
"update `$table` set " . $this->
updateSql($columns,$args) .
"\nwhere $where";
685 $result = $this->
execute($sql,$args);
686 if(!$result && $insert_if_not_exists) $result = $this->
insert($table,$columns);
697 public function inc($table,$columns,$where){
698 if(!is_array($columns)) $columns = [$columns => 1];
700 foreach($columns as $column => $value) $update[
'!' . $column] =
"`$column` + $value";
701 return $this->
upsert($table,$columns + $where,$update);
710 public function delete($table,$where,$args =
null){
711 $this->
scout($table);
712 $this->
logChanges($table,self::ACTION_DELETE,
false,$where,$args);
714 $sql =
"delete from `$table` where $where";
715 return $this->
execute($sql,$args);
724 public function exists($table,$where =
null,$args =
null){
726 $sql = $this->
limit(
"select 1 from `$table` where $where",1);
727 return (
bool)$this->
fetch($this->
query($sql,$args));
737 protected function aggregate($table,$function,$where =
null,$args =
null){
739 $sql =
"select $function from `$table` where $where";
740 $row = $this->
fetch($this->
query($sql,$args));
741 return array_pop($row);
750 public function count($table,$where =
null,$args =
null){
751 return $this->
aggregate($table,
'count(*)',$where,$args);
761 public function min($table,$column,$where =
null,$args =
null){
762 return $this->
aggregate($table,
"min(`$column`)",$where,$args);
772 public function max($table,$column,$where =
null,$args =
null){
773 return $this->
aggregate($table,
"max(`$column`)",$where,$args);
783 public function average($table,$column,$where =
null,$args =
null){
784 return $this->
aggregate($table,
"avg(`$column`)",$where,$args);
794 public function sum($table,$column,$where =
null,$args =
null){
795 return $this->
aggregate($table,
"sum(`$column`)",$where,$args);
802 return $this->
single(
'select database()');
809 return $this->
record(
'show tables');
817 public function columns($table,$database =
null){
822 CHARACTER_MAXIMUM_LENGTH as `length`,
823 NUMERIC_PRECISION as `precision`,
824 NUMERIC_SCALE as `scale`,
825 if(right(COLUMN_TYPE,8) = "unsigned",1,0) as `unsigned`,
826 COLLATION_NAME as `collation`,
827 COLUMN_DEFAULT as `default`,
828 if(IS_NULLABLE = "NO",1,0) as `required`,
829 if(COLUMN_KEY = "PRI",1,0) as `primary`,
831 select concat(REFERENCED_TABLE_NAME,".",REFERENCED_COLUMN_NAME)
832 from INFORMATION_SCHEMA.KEY_COLUMN_USAGE
833 where TABLE_SCHEMA = col.TABLE_SCHEMA
834 and TABLE_NAME = col.TABLE_NAME
835 and COLUMN_NAME = col.COLUMN_NAME
836 and REFERENCED_COLUMN_NAME is not null
838 from INFORMATION_SCHEMA.COLUMNS as col
839 where TABLE_SCHEMA = :database
840 and TABLE_NAME = :table
841 order by ORDINAL_POSITION',
843 'database' => $database ?: $this->
database(),
854 $this->
component(
'log')->debug(__CLASS__ .
"::optimize('$table')",__FILE__,__LINE__);
855 $this->
record(
"optimize table `$table`");
866 return $this->
all(
'explain ' . $sql,$args);
875 ID,USER as `user`,HOST as `host`,DB as `database`,
876 if(STATE = "",COMMAND,STATE) as `status`,TIME' . (stripos($this->version,
'MariaDB') ?
'_MS / 1000' :
'') .
' as `time`,
878 from INFORMATION_SCHEMA.PROCESSLIST';
879 return $this->
record($sql .
' where INFO not like :sql', [
'sql' => trim($sql) .
'%']);
888 $this->
execute(
'kill :id',[
'id' => $id]);
898 if(!$this->_migrate){
900 $this->_migrate =
new $class_name($this->_fred,$this->
config(
'migrate',[]) + [
'db' => $this]);
906 if(!$this->_pdo) $this->_pdo = $this->
createPdo();
911 if($this->_version ===
null)
912 $this->_version = \Rsi\Record::value($this->
record(
'show variables where variable_name="version"'),
false);
916 public function __call($func_name,$params){
918 if(substr($func_name,0,1) ==
'_'){
919 $func_name = substr($func_name,1);
920 $table = array_shift($params);
921 if(in_array($func_name,[
'insert',
'replace',
'select',
'update',
'delete',
'exists',
'count'])) array_unshift($params,$table);
923 $this->defTables = (array)$table;
925 $result = call_user_func_array([$this,$func_name],$params);
928 $this->defTables = $prev_tables;
932 if(is_numeric($where = array_shift($params)) || (is_string($where) && !preg_match(
'/[\\s=<>]/',$where)))
933 $where = [$this->
keyColumn($func_name) => $where];
934 $result = $this->
select($func_name,
'*',$where,array_shift($params),1);
940 return $this->
execute($sql,$args);