48 $connection = new \Rsi\Wrapper\Record($this->_connection);
49 $pdo = new \PDO($connection->dsn,$connection->username,$connection->password,($connection->options ?: []) + [
50 \PDO::MYSQL_ATTR_FOUND_ROWS =>
true 52 foreach(($attributes ?: []) + $this->_attributes + [
53 \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
54 \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
55 \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY =>
true 56 ] as $attribute => $value) $pdo->setAttribute($attribute,$value);
57 $this->
component(
'event')->trigger(self::EVENT_OPEN,$this,$pdo);
65 public function date($time = null){
66 return date(
'Y-m-d',$time ?: time());
74 return date(
'Y-m-d H:i:s',$time ?: time());
82 return !strcasecmp(substr(trim($sql),0,7),
'select ');
96 $this->pdo->beginTransaction();
102 $this->pdo->rollBack();
108 $this->pdo->commit();
119 call_user_func($callback,$this);
134 return $this->pdo->lastInsertId();
138 $this->_startTime = microtime(
true);
145 if(($time >= $this->statTime) && ($stats = $this->
component(
'stats')))
146 $stats->inc($this->statPrefix .
':' . trim(preg_replace(
'/\\s+/',
' ',$sql)),$time);
148 foreach($this->logTimes as $prio => $edge)
if($time >= $edge){
149 $log->add($prio,
'Slow query',compact(
'sql',
'args') + [
'time' => round($time,
max(0,$this->logTimeSignificant - ceil(log($time,10))))]);
152 if($this->_fred->debug){
153 if(array_key_exists($hash = md5($sql . serialize($args)),$this->_duploCount))
154 $log->info(
'Duplicate query' . ($this->_duploCount[$hash]++ ?
' (' . $this->_duploCount[$hash] .
')' :
''),__FILE__,__LINE__,compact(
'sql',
'args'));
155 else $this->_duploCount[$hash] = 0;
158 $this->allowInsert =
true;
162 if(!$args) $args = [];
163 else foreach($args as $key => $value)
164 if(!preg_match($pattern =
"/:$key\\b/",$sql)) unset($args[$key]);
165 elseif(is_array($value)){
166 if(!$value)
throw new \Exception(
"Empty array for '$key'");
169 foreach($value as $sub) $args[$keys[] = $key .
's' .
count($keys)] = $sub;
170 $sql = preg_replace($pattern,
':' . implode(
',:',$keys),$sql);
180 public function statement($sql,$args = null,$pdo = null){
181 if(!$pdo) $pdo = $this->pdo;
182 if($statement = $pdo->prepare($sql)){
183 if($args)
foreach($args as $key => $value){
184 $statement->bindValue(
':' . $key,$value);
187 $statement->execute();
198 public function execute($sql,$args = null,$pdo = null){
199 if($log = $this->
component(
'log')) $log->debug(__CLASS__ .
"::execute('$sql',args)",__FILE__,__LINE__,compact(
'sql',
'args'));
200 if(!$pdo) $pdo = $this->pdo;
204 if(!$args) $result = $pdo->exec($sql);
205 elseif($statement = $this->
statement($sql,$args,$pdo)){
206 $result = $statement->rowCount();
207 $statement->closeCursor();
220 public function query($sql,$args = null,$pdo = null){
221 if($log = $this->
component(
'log')) $log->debug(__CLASS__ .
"::query('$sql',args)",__FILE__,__LINE__,compact(
'sql',
'args'));
222 if(!$pdo) $pdo = $this->pdo;
225 $result = $args ? $this->
statement($sql,$args,$pdo) : $pdo->query($sql);
235 $row = $statement->fetch();
236 return $this->defTables ? $this->
component(
'def')->convertRecord($row,$this->defTables) : $row;
244 public function all($sql,$args = null){
245 if($statement = $this->
query($sql,$args)){
246 if(!$this->defTables)
return $statement->fetchAll();
248 while($row = $this->
fetch($statement)) $rows[] = $row;
249 $statement->closeCursor();
263 public function single($sql,$args = null,$auto =
true){
264 if($statement = $this->
query($sql,$args)){
265 $row = $this->
fetch($statement);
266 $statement->closeCursor();
268 if($row)
return $auto && (
count($row) == 1) ? array_pop($row) : $row;
281 public function record($sql,$args = null){
283 if($statement = $this->
query($sql,$args)){
284 while($row = $this->
fetch($statement)){
285 $key = array_shift($row);
287 case 0: $result[] = $key;
break;
288 case 1: $result[$key] = array_pop($row);
break;
289 default: $result[$key] = $row;
292 $statement->closeCursor();
307 public function each($callback,$sql,$args = null){
309 if($statement = $this->
query($sql,$args,$pdo = $this->
createPdo([\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY =>
false]))){
311 while($row = $this->
fetch($statement)){
313 if(call_user_func($callback,$row) ===
false)
break;
315 $statement->closeCursor();
327 $keys = $count =
false;
328 foreach($columns as $column => $value)
if(is_array($value)){
329 if($keys ===
false) $count =
count($keys = array_keys($value));
330 elseif(($count !=
count($value)) || array_diff($keys,array_keys($value)))
331 throw new \Exception(
"Different keys for column '$column'");
334 foreach((array)$keys as $key){
336 foreach($columns as $column => $value) $result[$key][$column] = is_array($value) ? $value[$key] : $value;
356 if($extra = $args && is_string($args) ?
"\n" . $args : null) $args = null;
357 if(is_array($where)){
358 if(!is_array($args)) $args = [];
359 $def = $this->defTables ? $this->
component(
'def') : null;
360 foreach($where as $column => &$value){
361 if($raw = substr($column,0,1) ==
'!') $column = substr($column,1);
363 if($operator = preg_match(
'/\\W+$/',$column,$match) ? $match[0] : null){
364 $column = substr($column,0,-strlen($operator));
366 case '<>': $negation =
' not';
break;
367 case '~': $operator =
'like';
break;
370 else $operator =
'=';
372 if($value === null)
switch($operator){
375 $value =
"`$column` is$negation null";
380 elseif(array_key_exists($operator,$this->multiOperators)){
381 $multi =
count($value) > 1;
382 $this->
prepareWhere($value,$args,$this->multiOperators[$operator]);
383 if($multi) $value =
"($value)";
385 elseif(is_array($value)){
387 if($def)
foreach($value as &$sub) $sub = $def->formatColumn($column,$sub,$this->defTables);
389 if(
count($value) == 1){
390 $args[$key =
'a' .
count($args)] = array_pop($value);
391 $value =
"`$column` $operator :$key";
394 $args[$key =
'a' .
count($args)] = $value;
398 $value =
"`$column`$negation in (:$key)";
401 throw new \DomainException(
"Invalid array operator '$operator' for column '$column'");
405 else $value = (int)($operator ==
'<>') .
'=1';
407 elseif($raw) $value =
"`$column` $operator $value";
409 $args[$key =
'v' .
count($args)] = $def ? $def->formatColumn($column,$value,$this->defTables) : $value;
410 $value =
"`$column` $operator :$key";
414 $where = implode(
" $glue ",$where);
416 $where = ($where ?:
'1=1') . $extra;
425 public function limit($sql,$limit,$offset = null){
426 return $sql .
' limit ' .
max(0,(
int)$offset) .
',' .
max(0,(
int)$limit);
436 protected function logChange($table,$action,$key,$old,$new){
437 $this->_fred->event->trigger(self::EVENT_LOG_CHANGE,$this,$table,$action,$key,$old,$new);
444 $this->_fred->event->trigger(self::EVENT_LOG_ADD_ID,$this,$this->_logAddId,$id);
454 protected function logChanges($table,$action,$columns,$where,$args = null){
455 $this->_logAddId =
false;
456 if($key_columns = $this->logTables[$table] ?? null)
try{
457 $key = is_array($where) ? \Rsi\Record::select($where,$key_columns) : [];
458 if($keys =
count($key) ==
count($key_columns) ? [$key] : $this->
select($table,$key_columns,$where,$args)){
459 foreach($keys as $key)
if(
460 ($current = $this->
select($table,$columns ===
false ?
'*' : array_keys($columns),$key,null,1)) &&
461 ($changed = array_diff_assoc($current,$columns ===
false ? $key : $columns))
462 ) $this->
logChange($table,$action,$key,$changed,$columns ===
false ?
false : array_intersect_key($columns,$changed));
464 elseif($action != self::ACTION_DELETE){
465 $this->
logChange($table,$action,
false,
false,$columns);
466 if(
count($key_columns) == 1) $this->_logAddId = array_pop($key_columns);
470 if($this->_fred->debug)
throw $e;
472 "Could not log '$action' on '$table': " . $e->getMessage(),
475 compact(
'columns',
'where',
'args')
487 public function scout($table,$columns = null,$where = null,$args = null){
490 if(!array_key_exists($table,$this->_scout)) $this->_scout[$table] = [];
491 if(!array_key_exists($key = serialize($columns),$this->_scout[$table])) $this->_scout[$table][$key] = [];
492 foreach(($result = $this->
select($table,$columns,$where,$args)) as $record) $this->_scout[$table][$key][serialize($record)] = $record;
494 else unset($this->_scout[$table]);
505 public function insert($table,$columns,$replace_if_exists =
false){
506 $this->
scout($table);
507 $def = $this->defTables ? $this->
component(
'def') : null;
511 foreach($columns as $column => &$value){
512 if($raw = substr($column,0,1) ==
'!') $column = substr($column,1);
513 elseif($def) $value = $def->formatColumn($column,$value,$this->defTables);
514 $values[$column] = $raw ? $value :
':' . $column;
517 $this->
logChanges($table,$action = $replace_if_exists ? self::ACTION_REPLACE : self::ACTION_INSERT,$columns,$columns);
518 $sql =
"$action into `$table` (`" . implode(
'`,`',array_keys($values)) .
"`)\nvalues (" . implode(
',',$values) .
')';
519 if($this->
execute($sql,$columns)){
521 if($this->_logAddId && ($id = $this->
lastInsertId()))
try{
525 if($this->_fred->debug)
throw $e;
539 return $this->
insert($table,$columns,
true);
552 public function select($table,$columns =
'*',$where = null,$args = null,$limit = null,$offset = null){
554 ($limit == 1) && !$offset &&
555 array_key_exists($table,$this->_scout) && array_key_exists($key = serialize($columns),$this->_scout[$table]) &&
556 is_array($where) && !array_filter($where,
function($value){
557 return is_array($value);
559 )
foreach($this->_scout[$table][$key] as $record){
560 foreach($where as $key => $value)
if(($record[$key] ??
false) !== $value)
continue 2;
564 $columns = preg_replace(
'/^(`)?\\+/',
'$1',is_array($columns) ?
'`' . implode(
'`,`',$columns) .
'`' : $columns,1,$record);
565 $sql =
"select $columns from `$table` where $where";
566 if($limit) $sql = $this->
limit($sql,$limit,$offset);
567 if($record)
return $this->
record($sql,$args);
568 $rows = $this->
all($sql,$args);
569 return $limit ===
true 570 ? (($row = array_shift($rows)) ? array_shift($row) :
false)
571 : ($rows ? ($limit == 1 ? array_shift($rows) : $rows) :
false);
582 public function update($table,$columns,$where = null,$args = null,$insert_if_not_exists =
false){
583 $this->
scout($table);
584 $this->
logChanges($table,self::ACTION_UPDATE,$columns,$where,$args);
586 $def = $this->defTables ? $this->
component(
'def') : null;
588 foreach($columns as $column => $value){
589 if($raw = substr($column,0,1) ==
'!') $column = substr($column,1);
590 elseif($def) $value = $def->formatColumn($column,$value,$this->defTables);
591 $args[$key =
'c' .
count($args)] = $value;
592 $sql[] =
"`$column` = " . ($raw ? $value :
':' . $key);
594 $sql =
"update `$table` set " . implode(
',',$sql) .
" where $where";
595 $result = $this->
execute($sql,$args);
596 if(!$result && $insert_if_not_exists) $result = $this->
insert($table,$columns);
606 public function delete($table,$where = null,$args = null){
607 $this->
scout($table);
608 $this->
logChanges($table,self::ACTION_DELETE,
false,$where,$args);
610 $sql =
"delete from `$table` where $where";
611 return $this->
execute($sql,$args);
620 public function exists($table,$where = null,$args = null){
622 $sql = $this->
limit(
"select 1 from `$table` where $where",1);
623 return (
bool)$this->
fetch($this->
query($sql,$args));
626 protected function aggregate($table,$function,$where = null,$args = null){
628 $sql =
"select $function from `$table` where $where";
629 $row = $this->
fetch($this->
query($sql,$args));
630 return array_pop($row);
639 public function count($table,$where = null,$args = null){
640 return $this->
aggregate($table,
'count(*)',$where,$args);
650 public function min($table,$column,$where = null,$args = null){
651 return $this->
aggregate($table,
"min(`$column`)",$where,$args);
661 public function max($table,$column,$where = null,$args = null){
662 return $this->
aggregate($table,
"max(`$column`)",$where,$args);
672 public function average($table,$column,$where = null,$args = null){
673 return $this->
aggregate($table,
"avg(`$column`)",$where,$args);
683 public function sum($table,$column,$where = null,$args = null){
684 return $this->
aggregate($table,
"sum(`$column`)",$where,$args);
691 return $this->
single(
'select database()');
698 return $this->
record(
'show tables');
706 public function columns($table,$database = null){
711 CHARACTER_MAXIMUM_LENGTH as `length`, 712 NUMERIC_PRECISION as `precision`, 713 NUMERIC_SCALE as `scale`, 714 if(right(COLUMN_TYPE,8) = "unsigned",1,0) as `unsigned`, 715 COLLATION_NAME as `collation`, 716 COLUMN_DEFAULT as `default`, 717 if(IS_NULLABLE = "NO",1,0) as `required`, 718 if(COLUMN_KEY = "PRI",1,0) as `primary`, 720 select concat(REFERENCED_TABLE_NAME,".",REFERENCED_COLUMN_NAME) 721 from INFORMATION_SCHEMA.KEY_COLUMN_USAGE 722 where TABLE_SCHEMA = col.TABLE_SCHEMA 723 and TABLE_NAME = col.TABLE_NAME 724 and COLUMN_NAME = col.COLUMN_NAME 725 and REFERENCED_COLUMN_NAME is not null 727 from INFORMATION_SCHEMA.COLUMNS as col 728 where TABLE_SCHEMA = :database 729 and TABLE_NAME = :table 730 order by ORDINAL_POSITION',
732 'database' => $database ?: $this->
database(),
743 $this->
component(
'log')->debug(__CLASS__ .
"::optimize('$table')",__FILE__,__LINE__);
744 $this->
createPdo()->exec(
"optimize table `$table`");
755 return $this->
all(
'explain ' . $sql,$args);
764 ID,USER as `user`,HOST as `host`,DB as `database`, 765 if(STATE = "",COMMAND,STATE) as `status`,TIME' . (stripos($this->
version,
'MariaDB') ?
'_MS / 1000' :
'') .
' as `time`, 767 from INFORMATION_SCHEMA.PROCESSLIST';
768 return $this->
record($sql .
' where INFO not like :sql', [
'sql' => trim($sql) .
'%']);
777 $this->
execute(
'kill :id',[
'id' => $id]);
787 if(!$this->_migrate){
789 $this->_migrate =
new $class_name($this->_fred,$this->
config(
'migrate',[]) + [
'db' => $this]);
795 if(!$this->_pdo) $this->_pdo = $this->
createPdo();
800 if($this->_version === null)
801 $this->_version = \Rsi\Record::value($this->
record(
'show variables where variable_name="version"'),
false);
805 public function __call($func_name,$params){
807 if(substr($func_name,0,1) ==
'_'){
808 $func_name = substr($func_name,1);
809 $table = array_shift($params);
810 if(in_array($func_name,[
'insert',
'replace',
'select',
'update',
'delete',
'exists',
'count'])) array_unshift($params,$table);
812 $this->defTables = (array)$table;
814 $result = call_user_func_array([$this,$func_name],$params);
817 $this->defTables = $prev_tables;
821 if(is_numeric($where = array_shift($params)) || (is_string($where) && !preg_match(
'/[\\s=<>]/',$where)))
822 $where = [$this->
keyColumn($func_name) => $where];
823 $result = $this->
select($func_name,
'*',$where,array_shift($params),1);
829 return $this->
execute($sql,$args);
explain($sql, $args=null)
Explain plan.
insert($table, $columns, $replace_if_exists=false)
Insert a record.
average($table, $column, $where=null, $args=null)
Average value from records that meet the requirements.
processes()
Running processes.
min($table, $column, $where=null, $args=null)
Smallest value from records that meet the requirements.
aggregate($table, $function, $where=null, $args=null)
replace($table, $columns)
Replace a record (insert if not exists).
lastInsertId()
Get the last auto-incremented ID.
count($table, $where=null, $args=null)
The number of records that meet the requirements.
logAddId($id)
Add a auto-increment ID to the latest change log.
each($callback, $sql, $args=null)
Run a callback function for every row in an SQL resultset.
$queryTime
Total time spent on queries (seconds).
logChanges($table, $action, $columns, $where, $args=null)
Log the changes for an action.
__call($func_name, $params)
exists($table, $where=null, $args=null)
Check if a record exists that meets the requirement.
config($key, $default=null)
Retrieve a config value.
scout($table, $columns=null, $where=null, $args=null)
Pre-select records (will be used by select()).
__invoke($sql, $args=null)
version(&$hash=null)
Version without hash.
transaction($callback, $throw=true)
Wrap a callback function in a transaction.
const ERROR
Error conditions.
prepareArgs(&$sql, &$args)
fetch($statement)
Fetch a row from an SQL statement.
begin()
Begin a transaction.
all($sql, $args=null)
Return al rows from an SQL statement.
const CRITICAL
Critical conditions.
date($time=null)
Convert a Unix timestamp to database date format.
$migrateClassName
Class name for the migration tool.
optimize($table=null)
Optimize table(s).
rollBack()
Roll a transaction back.
prepareWhere(&$where, &$args=null, $glue='and')
Prepare a where statement.
$logTables
Tables (key) and key columns (value) to log changes for.
$defTables
Table definition to use (table name).
sum($table, $column, $where=null, $args=null)
Total value from records that meet the requirements.
isSelection($sql)
Check if an SQL statement is a select statement.
$allowInsert
Allow inserts (for secondary processes like logs).
const WARNING
Warning conditions.
database()
Name of the current database.
query($sql, $args=null, $pdo=null)
Execute an SQL statement.
multiColumns($columns)
Expand an array with scalar values and arrays to multiple arrays.
$logTimes
Query time above which to add a note to the log (key = prio, value = edge; higher times first)...
commit()
Commit a transaction.
dateTime($time=null)
Convert a Unix timestamp to database date+time format.
columns($table, $database=null)
Column properties for a table.
update($table, $columns, $where=null, $args=null, $insert_if_not_exists=false)
Update one or more record(s).
single($sql, $args=null, $auto=true)
Return a single row from an SQL statement.
$logTimeSignificant
Number of significant figures (rounding).
createPdo($attributes=null)
record($sql, $args=null)
Returns an array from an SQL statement.
max($table, $column, $where=null, $args=null)
Biggest value from records that meet the requirements.
logChange($table, $action, $key, $old, $new)
Log a change.
$queryCount
Number of queries executed.
keyColumn($table)
Primary key column.
limit($sql, $limit, $offset=null)
Limit an SQL statement.
execute($sql, $args=null, $pdo=null)
Execute an SQL statement.
component($name)
Get a component (local or default).
select($table, $columns=' *', $where=null, $args=null, $limit=null, $offset=null)
Select records.
tables()
All tables in current database.
statement($sql, $args=null, $pdo=null)
Create a PDO statement.
$statTime
Operators (key) and glue (value) for multi column where conditions.