FRED™  3.0
FRED™: Framework for Rapid and Easy Development
Db.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Rsi\Fred;
4 
5 /**
6  * Database access layer (PDO based).
7  */
8 class Db extends Component{
9 
10  const ACTION_INSERT = 'insert';
11  const ACTION_REPLACE = 'replace';
12  const ACTION_UPDATE = 'update';
13  const ACTION_DELETE = 'delete';
14 
15  const EVENT_OPEN = 'db:open';
16  const EVENT_LOG_CHANGE = 'db:logChange';
17  const EVENT_LOG_ADD_ID = 'db:logAddId';
18 
19  public $multiOperators = ['^' => 'or','&' => 'and']; //!< Operators (key) and glue (value) for multi column where conditions.
20  public $statTime = 3600; //!< Query time above which to increment a stat counter for the specific query (id = SQL).
21  public $statPrefix = 'db';
22  public $logTimes = [ //!< Query time above which to add a note to the log (key = prio, value = edge; higher times first).
23  Log::CRITICAL => 10.0,
24  Log::ERROR => 5.0,
25  Log::WARNING => 2.5
26  ];
27  public $logTimeSignificant = 3; //!< Number of significant figures (rounding).
28  public $logTables = []; //!< Tables (key) and key columns (value) to log changes for.
29  public $defTables = null; //!< Table definition to use (table name).
30  public $migrateClassName = __CLASS__ . '\\Migrate'; //!< Class name for the migration tool.
31 
32  public $queryCount = 0; //!< Number of queries executed.
33  public $queryTime = 0; //!< Total time spent on queries (seconds).
34  public $allowInsert = true; //!< Allow inserts (for secondary processes like logs).
35 
36  protected $_connection = [];
37  protected $_attributes = [];
38  protected $_version = null;
39  protected $_pdo = null;
40  protected $_migrate = null;
41 
42  protected $_startTime = null;
43  protected $_duploCount = [];
44  protected $_logAddId = false;
45  protected $_scout = [];
46 
47  public function createPdo($attributes = null){
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
51  ]);
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);
58  return $pdo;
59  }
60  /**
61  * Convert a Unix timestamp to database date format.
62  * @param int $time Timestamp (empty = now).
63  * @return string Date as a string.
64  */
65  public function date($time = null){
66  return date('Y-m-d',$time ?: time());
67  }
68  /**
69  * Convert a Unix timestamp to database date+time format.
70  * @param int $time Timestamp (empty = now).
71  * @return string Date+time as a string.
72  */
73  public function dateTime($time = null){
74  return date('Y-m-d H:i:s',$time ?: time());
75  }
76  /**
77  * Check if an SQL statement is a select statement.
78  * @param string $sql SQL statement.
79  * @return bool True if it is a select statement.
80  */
81  public function isSelection($sql){
82  return !strcasecmp(substr(trim($sql),0,7),'select ');
83  }
84  /**
85  * Primary key column.
86  * @param string $table For this table.
87  * @return string Column name.
88  */
89  public function keyColumn($table){
90  return 'id';
91  }
92  /**
93  * Begin a transaction.
94  */
95  public function begin(){
96  $this->pdo->beginTransaction();
97  }
98  /**
99  * Roll a transaction back.
100  */
101  public function rollBack(){
102  $this->pdo->rollBack();
103  }
104  /**
105  * Commit a transaction.
106  */
107  public function commit(){
108  $this->pdo->commit();
109  }
110  /**
111  * Wrap a callback function in a transaction.
112  * @param callable $callback Callback function (first and only parameter is this instance).
113  * @param bool $throw If true, an exception is re-thrown after the rollback.
114  * @return bool True on success, false on error (and $throw set to false).
115  */
116  public function transaction($callback,$throw = true){
117  $this->begin();
118  try{
119  call_user_func($callback,$this);
120  $this->commit();
121  }
122  catch(\Exception $e){
123  $this->rollBack();
124  if($throw) throw $e;
125  return false;
126  }
127  return true;
128  }
129  /**
130  * Get the last auto-incremented ID.
131  * @return string
132  */
133  public function lastInsertId(){
134  return $this->pdo->lastInsertId();
135  }
136 
137  protected function startTimer(){
138  $this->_startTime = microtime(true);
139  }
140 
141  protected function checkTimer($sql,$args){
142  $this->queryCount++;
143  $this->queryTime += ($time = microtime(true) - $this->_startTime);
144  $this->allowInsert = $this->isSelection($sql);
145  if(($time >= $this->statTime) && ($stats = $this->component('stats')))
146  $stats->inc($this->statPrefix . ':' . trim(preg_replace('/\\s+/',' ',$sql)),$time);
147  if($log = $this->component('log')){
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))))]);
150  break;
151  }
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;
156  }
157  }
158  $this->allowInsert = true;
159  }
160 
161  protected function prepareArgs(&$sql,&$args){
162  if(!$args) $args = [];
163  else foreach($args as $key => $value)
164  if(!preg_match($pattern = "/:$key\\b/",$sql)) unset($args[$key]); //not used in SQL statement
165  elseif(is_array($value)){
166  if(!$value) throw new \Exception("Empty array for '$key'");
167  unset($args[$key]);
168  $keys = [];
169  foreach($value as $sub) $args[$keys[] = $key . 's' . count($keys)] = $sub;
170  $sql = preg_replace($pattern,':' . implode(',:',$keys),$sql);
171  }
172  }
173  /**
174  * Create a PDO statement.
175  * @param string $sql SQL statement to execute.
176  * @param array $args Variables to bind to the statement.
177  * @param PDO $pdo Specific PDO connection to use.
178  * @return PDOStatement (false on failure).
179  */
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);
185  unset($value);
186  }
187  $statement->execute();
188  }
189  return $statement;
190  }
191  /**
192  * Execute an SQL statement
193  * @param string $sql SQL statement to execute.
194  * @param array $args Variables to bind to the statement.
195  * @param PDO $pdo Specific PDO connection to use.
196  * @return int The number of affected rows.
197  */
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;
201  $result = null;
202  $this->prepareArgs($sql,$args);
203  $this->startTimer();
204  if(!$args) $result = $pdo->exec($sql);
205  elseif($statement = $this->statement($sql,$args,$pdo)){
206  $result = $statement->rowCount();
207  $statement->closeCursor();
208  $statement = null;
209  }
210  $this->checkTimer($sql,$args);
211  return $result;
212  }
213  /**
214  * Execute an SQL statement.
215  * @param string $sql SQL statement to execute.
216  * @param array $args Variables to bind to the statement.
217  * @param PDO $pdo Specific PDO connection to use.
218  * @return PDOStatement (false on failure).
219  */
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;
223  $this->prepareArgs($sql,$args);
224  $this->startTimer();
225  $result = $args ? $this->statement($sql,$args,$pdo) : $pdo->query($sql);
226  $this->checkTimer($sql,$args);
227  return $result;
228  }
229  /**
230  * Fetch a row from an SQL statement.
231  * @param PDOStatement $statement
232  * @return array
233  */
234  public function fetch($statement){
235  $row = $statement->fetch();
236  return $this->defTables ? $this->component('def')->convertRecord($row,$this->defTables) : $row;
237  }
238  /**
239  * Return al rows from an SQL statement.
240  * @param string $sql SQL statement to execute.
241  * @param array $args Variables to bind to the statement.
242  * @return array All rows (false on failure).
243  */
244  public function all($sql,$args = null){
245  if($statement = $this->query($sql,$args)){
246  if(!$this->defTables) return $statement->fetchAll();
247  $rows = [];
248  while($row = $this->fetch($statement)) $rows[] = $row;
249  $statement->closeCursor();
250  $statement = null;
251  return $rows;
252  }
253  return false;
254  }
255  /**
256  * Return a single row from an SQL statement.
257  * Returns a single value if the resulting row has only one column.
258  * @param string $sql SQL statement to execute.
259  * @param array $args Variables to bind to the statement.
260  * @param bool $auto Set to false to always return a row.
261  * @return mixed Row (multi column) or value (single column).
262  */
263  public function single($sql,$args = null,$auto = true){
264  if($statement = $this->query($sql,$args)){
265  $row = $this->fetch($statement);
266  $statement->closeCursor();
267  $statement = null;
268  if($row) return $auto && (count($row) == 1) ? array_pop($row) : $row;
269  }
270  return false;
271  }
272  /**
273  * Returns an array from an SQL statement.
274  * If the query returns only one column, the result is an array of those values. With two columns, the first one becomes the
275  * key, and the second one the value of an assoc.array. With three or more columns the first column will be the key, and the
276  * others the value.
277  * @param string $sql SQL statement to execute.
278  * @param array $args Variables to bind to the statement.
279  * @return array
280  */
281  public function record($sql,$args = null){
282  $result = [];
283  if($statement = $this->query($sql,$args)){
284  while($row = $this->fetch($statement)){
285  $key = array_shift($row);
286  switch(count($row)){
287  case 0: $result[] = $key; break;
288  case 1: $result[$key] = array_pop($row); break;
289  default: $result[$key] = $row;
290  }
291  }
292  $statement->closeCursor();
293  $statement = null;
294  }
295  return $result;
296  }
297  /**
298  * Run a callback function for every row in an SQL resultset.
299  * A seperate conection is used without buffering. This makes it possible to process huge datasets one record at a time,
300  * without running out of memory.
301  * @param callable $callback Callback function (first and only parameter is the row). If the function returns explicitly
302  * false the execution of further rows is halted.
303  * @param string $sql SQL statement to execute.
304  * @param array $args Variables to bind to the statement.
305  * @return int Number of records processed (false on error).
306  */
307  public function each($callback,$sql,$args = null){
308  $result = false;
309  if($statement = $this->query($sql,$args,$pdo = $this->createPdo([\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false]))){
310  $result = 0;
311  while($row = $this->fetch($statement)){
312  $result++;
313  if(call_user_func($callback,$row) === false) break;
314  }
315  $statement->closeCursor();
316  $statement = null;
317  }
318  $pdo = null;
319  return $result;
320  }
321  /**
322  * Expand an array with scalar values and arrays to multiple arrays.
323  * @param array $columns Columns (key = column name, value = array with values or single scalar value).
324  * @return array Array of column arrays (key = column name, value = value).
325  */
326  public function multiColumns($columns){
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'");
332  }
333  $result = [];
334  foreach((array)$keys as $key){
335  $result[$key] = [];
336  foreach($columns as $column => $value) $result[$key][$column] = is_array($value) ? $value[$key] : $value;
337  }
338  return $result;
339  }
340  /**
341  * Prepare a where statement.
342  * @param string|array $where If this a string, it is used directly as the where clause. If it is an assoc.array, it is
343  * translated to a where statement. Key => value pairs are translated as follow:
344  * - 'key' => 'value' : "key = 'value'" (or "key is null" when the value is null, or "key in (...)" when the value is an
345  * array).
346  * - 'key<>' => 'value' : "key <> 'value'" (or "key is not null" when the value is null, or "key not in (...)" when the
347  * value is an array).
348  * - 'key~' => 'value' : "key like 'value'"
349  * - 'key^' => [key1 => value1,key2 => value2] : "(key1 = value1 or key2 = value2)" (same rules as above apply for the 'or'
350  * part).
351  * @param string|array $args If the $where is a string, and this is also, this value is added to the where. Otherwise the
352  * arguments resulting from the translation will be added to this array.
353  * @param string $glue Glue to combine seperate arguments.
354  */
355  public function prepareWhere(&$where,&$args = null,$glue = 'and'){
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);
362  $negation = '';
363  if($operator = preg_match('/\\W+$/',$column,$match) ? $match[0] : null){
364  $column = substr($column,0,-strlen($operator));
365  switch($operator){
366  case '<>': $negation = ' not'; break;
367  case '~': $operator = 'like'; break;
368  }
369  }
370  else $operator = '=';
371 
372  if($value === null) switch($operator){
373  case '=':
374  case '<>':
375  $value = "`$column` is$negation null";
376  break;
377  default:
378  $value = '0=1'; //undefined
379  }
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)";
384  }
385  elseif(is_array($value)){
386  if($value){
387  if($def) foreach($value as &$sub) $sub = $def->formatColumn($column,$sub,$this->defTables);
388  unset($sub);
389  if(count($value) == 1){
390  $args[$key = 'a' . count($args)] = array_pop($value);
391  $value = "`$column` $operator :$key";
392  }
393  else{
394  $args[$key = 'a' . count($args)] = $value;
395  switch($operator){
396  case '=':
397  case '<>':
398  $value = "`$column`$negation in (:$key)";
399  break;
400  default:
401  throw new \DomainException("Invalid array operator '$operator' for column '$column'");
402  }
403  }
404  }
405  else $value = (int)($operator == '<>') . '=1';
406  }
407  elseif($raw) $value = "`$column` $operator $value";
408  else{
409  $args[$key = 'v' . count($args)] = $def ? $def->formatColumn($column,$value,$this->defTables) : $value;
410  $value = "`$column` $operator :$key";
411  }
412  }
413  unset($value);
414  $where = implode(" $glue ",$where);
415  }
416  $where = ($where ?: '1=1') . $extra;
417  }
418  /**
419  * Limit an SQL statement.
420  * @param string $sql Original SQL statement.
421  * @param int $limit Limit (number of rows).
422  * @param int $offset Offset.
423  * @return string SQL statement with limit.
424  */
425  public function limit($sql,$limit,$offset = null){
426  return $sql . ' limit ' . max(0,(int)$offset) . ',' . max(0,(int)$limit);
427  }
428  /**
429  * Log a change.
430  * @param string $table Table name.
431  * @param string $action Change type (see ACTION_* constants).
432  * @param array $key Key of the changed record (key = column name, value = value).
433  * @param array $old Original values for changed columns (key = column name, value = value).
434  * @param array $new New values for changed columns (key = column name, value = value).
435  */
436  protected function logChange($table,$action,$key,$old,$new){
437  $this->_fred->event->trigger(self::EVENT_LOG_CHANGE,$this,$table,$action,$key,$old,$new);
438  }
439  /**
440  * Add a auto-increment ID to the latest change log.
441  * @param string $id The last auto-incremented ID (the key is stored in $this->_logAddId).
442  */
443  protected function logAddId($id){
444  $this->_fred->event->trigger(self::EVENT_LOG_ADD_ID,$this,$this->_logAddId,$id);
445  }
446  /**
447  * Log the changes for an action.
448  * @param string $table Table name.
449  * @param string $action Change type (see ACTION_* constants).
450  * @param array $columns New values (key = column name, value = value).
451  * @param string|array $where Where.
452  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
453  */
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));
463  }
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);
467  }
468  }
469  catch(\Exception $e){
470  if($this->_fred->debug) throw $e;
471  $this->component('log')->critical(
472  "Could not log '$action' on '$table': " . $e->getMessage(),
473  __FILE__,
474  __LINE__,
475  compact('columns','where','args')
476  );
477  }
478  }
479  /**
480  * Pre-select records (will be used by select()).
481  * @param string $table Table name.
482  * @param array $columns Columns to select from the table. Leaving this empty will clear the cache for this table.
483  * @param string|array $where Where.
484  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
485  * @return array Selected rows (null on clear).
486  */
487  public function scout($table,$columns = null,$where = null,$args = null){
488  $result = null;
489  if($columns){
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;
493  }
494  else unset($this->_scout[$table]);
495  return $result;
496  }
497  /**
498  * Insert a record.
499  * @param string $table Table name.
500  * @param array $columns Columns (key = column name, value = value or values - for multiple records).
501  * @param bool $replace_if_exists If true, an existing record will be replaced (if alse, an exception will be thrown when a
502  * record with the same key already exists).
503  * @return int The number of affected rows (0 = failure, 1 = success).
504  */
505  public function insert($table,$columns,$replace_if_exists = false){
506  $this->scout($table);
507  $def = $this->defTables ? $this->component('def') : null;
508  $result = 0;
509  foreach($this->multiColumns($columns) as $columns){
510  $values = [];
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;
515  }
516  unset($value);
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)){
520  $result++;
521  if($this->_logAddId && ($id = $this->lastInsertId())) try{
522  $this->logAddId($id);
523  }
524  catch(\Exception $e){
525  if($this->_fred->debug) throw $e;
526  $this->component('log')->critical($e);
527  }
528  }
529  }
530  return $result;
531  }
532  /**
533  * Replace a record (insert if not exists).
534  * @param string $table Table name.
535  * @param array $columns Columns (key = column name, value = value or values - for multiple records).
536  * @return int The number of affected rows (0 = failure, 1 = success).
537  */
538  public function replace($table,$columns){
539  return $this->insert($table,$columns,true);
540  }
541  /**
542  * Select records.
543  * @param string $table Table name.
544  * @param array $columns Columns to select from the table. Prefix the first columns with a '+' to make this the key.
545  * @param string|array $where Where.
546  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
547  * @param int|bool $limit Limit the number of rows (0 = no limit).
548  * @param int $offset Offset.
549  * @return array Single value when limit is true, single row when limit is 1, otherwise an array of records.
550  * @see prepareWhere()
551  */
552  public function select($table,$columns = '*',$where = null,$args = null,$limit = null,$offset = null){
553  if(
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);
558  })
559  ) foreach($this->_scout[$table][$key] as $record){
560  foreach($where as $key => $value) if(($record[$key] ?? false) !== $value) continue 2;
561  return $record;
562  }
563  $this->prepareWhere($where,$args);
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) //single column
571  : ($rows ? ($limit == 1 ? array_shift($rows) : $rows) : false);
572  }
573  /**
574  * Update one or more record(s).
575  * @param string $table Table name.
576  * @param array $columns Columns to update (key = column name, value = new value).
577  * @param string|array $where Where.
578  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
579  * @param bool $insert_if_not_exists If true, a new record will be inserted when there is not yet an existing record.
580  * @return int The number of affected rows (0 = failure, 1 or more = success).
581  */
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);
585  $this->prepareWhere($where,$args);
586  $def = $this->defTables ? $this->component('def') : null;
587  $sql = [];
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);
593  }
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);
597  return $result;
598  }
599  /**
600  * Delete one or more record(s).
601  * @param string $table Table name.
602  * @param string|array $where Where.
603  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
604  * @return int The number of affected rows (0 = failure, 1 or more = success).
605  */
606  public function delete($table,$where = null,$args = null){
607  $this->scout($table);
608  $this->logChanges($table,self::ACTION_DELETE,false,$where,$args);
609  $this->prepareWhere($where,$args);
610  $sql = "delete from `$table` where $where";
611  return $this->execute($sql,$args);
612  }
613  /**
614  * Check if a record exists that meets the requirement.
615  * @param string $table Table name.
616  * @param string|array $where Where.
617  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
618  * @return bool True if a record exists.
619  */
620  public function exists($table,$where = null,$args = null){
621  $this->prepareWhere($where,$args);
622  $sql = $this->limit("select 1 from `$table` where $where",1);
623  return (bool)$this->fetch($this->query($sql,$args));
624  }
625 
626  protected function aggregate($table,$function,$where = null,$args = null){
627  $this->prepareWhere($where,$args);
628  $sql = "select $function from `$table` where $where";
629  $row = $this->fetch($this->query($sql,$args));
630  return array_pop($row);
631  }
632  /**
633  * The number of records that meet the requirements.
634  * @param string $table Table name.
635  * @param string|array $where Where.
636  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
637  * @return int Number of rows.
638  */
639  public function count($table,$where = null,$args = null){
640  return $this->aggregate($table,'count(*)',$where,$args);
641  }
642  /**
643  * Smallest value from records that meet the requirements.
644  * @param string $table Table name.
645  * @param string $column Column to get value from.
646  * @param string|array $where Where.
647  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
648  * @return mixed Smallest value.
649  */
650  public function min($table,$column,$where = null,$args = null){
651  return $this->aggregate($table,"min(`$column`)",$where,$args);
652  }
653  /**
654  * Biggest value from records that meet the requirements.
655  * @param string $table Table name.
656  * @param string $column Column to get value from.
657  * @param string|array $where Where.
658  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
659  * @return mixed Biggest value.
660  */
661  public function max($table,$column,$where = null,$args = null){
662  return $this->aggregate($table,"max(`$column`)",$where,$args);
663  }
664  /**
665  * Average value from records that meet the requirements.
666  * @param string $table Table name.
667  * @param string $column Column to get value from.
668  * @param string|array $where Where.
669  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
670  * @return mixed Average value.
671  */
672  public function average($table,$column,$where = null,$args = null){
673  return $this->aggregate($table,"avg(`$column`)",$where,$args);
674  }
675  /**
676  * Total value from records that meet the requirements.
677  * @param string $table Table name.
678  * @param string $column Column to get value from.
679  * @param string|array $where Where.
680  * @param string|array $args Arguments (assoc.array) or extra statement for the where.
681  * @return mixed Total value.
682  */
683  public function sum($table,$column,$where = null,$args = null){
684  return $this->aggregate($table,"sum(`$column`)",$where,$args);
685  }
686  /**
687  * Name of the current database.
688  * @return string
689  */
690  public function database(){
691  return $this->single('select database()');
692  }
693  /**
694  * All tables in current database.
695  * @return array
696  */
697  public function tables(){
698  return $this->record('show tables');
699  }
700  /**
701  * Column properties for a table.
702  * @param string $table Name of the table.
703  * @param string $database Name of the database (current database when empty).
704  * @return array Key = column name, value = assoc.array with properties.
705  */
706  public function columns($table,$database = null){
707  return $this->record('
708  select
709  COLUMN_NAME,
710  DATA_TYPE as `type`,
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`,
719  (
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
726  ) as `ref`
727  from INFORMATION_SCHEMA.COLUMNS as col
728  where TABLE_SCHEMA = :database
729  and TABLE_NAME = :table
730  order by ORDINAL_POSITION',
731  [
732  'database' => $database ?: $this->database(),
733  'table' => $table
734  ]
735  );
736  }
737  /**
738  * Optimize table(s).
739  * @param string $table Table to optimize (empty for all tables).
740  */
741  public function optimize($table = null){
742  if($table){
743  $this->component('log')->debug(__CLASS__ . "::optimize('$table')",__FILE__,__LINE__);
744  $this->createPdo()->exec("optimize table `$table`");
745  }
746  else foreach($this->tables() as $table) $this->optimize($table);
747  }
748  /**
749  * Explain plan.
750  * @param string $sql SQL statement to explain.
751  * @param array $args Variables to bind to the statement.
752  * @return array Steps in the plan.
753  */
754  public function explain($sql,$args = null){
755  return $this->all('explain ' . $sql,$args);
756  }
757  /**
758  * Running processes.
759  * @return array Key = process ID, value = array with info.
760  */
761  public function processes(){
762  $sql = '
763  select
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`,
766  INFO as `sql`
767  from INFORMATION_SCHEMA.PROCESSLIST';
768  return $this->record($sql . ' where INFO not like :sql', ['sql' => trim($sql) . '%']);
769  }
770  /**
771  * Kill a process
772  * @param int $id Process ID.
773  * @return bool True on success.
774  */
775  public function kill($id){
776  try{
777  $this->execute('kill :id',['id' => $id]);
778  return true;
779  }
780  catch(\Exception $e){
781  $this->component('log')->info($e);
782  }
783  return false;
784  }
785 
786  protected function getMigrate(){
787  if(!$this->_migrate){
788  $class_name = $this->migrateClassName;
789  $this->_migrate = new $class_name($this->_fred,$this->config('migrate',[]) + ['db' => $this]);
790  }
791  return $this->_migrate;
792  }
793 
794  protected function getPdo(){
795  if(!$this->_pdo) $this->_pdo = $this->createPdo();
796  return $this->_pdo;
797  }
798 
799  protected function getVersion(){
800  if($this->_version === null)
801  $this->_version = \Rsi\Record::value($this->record('show variables where variable_name="version"'),false);
802  return $this->_version;
803  }
804 
805  public function __call($func_name,$params){
806  $result = false;
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);
811  $prev_tables = $this->defTables;
812  $this->defTables = (array)$table;
813  try{
814  $result = call_user_func_array([$this,$func_name],$params);
815  }
816  finally{
817  $this->defTables = $prev_tables;
818  }
819  }
820  else{
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);
824  }
825  return $result;
826  }
827 
828  public function __invoke($sql,$args = null){
829  return $this->execute($sql,$args);
830  }
831 
832  public function __sleep(){
833  return [];
834  }
835 
836 }
explain($sql, $args=null)
Explain plan.
Definition: Db.php:754
const ACTION_UPDATE
Definition: Db.php:12
insert($table, $columns, $replace_if_exists=false)
Insert a record.
Definition: Db.php:505
average($table, $column, $where=null, $args=null)
Average value from records that meet the requirements.
Definition: Db.php:672
processes()
Running processes.
Definition: Db.php:761
min($table, $column, $where=null, $args=null)
Smallest value from records that meet the requirements.
Definition: Db.php:650
aggregate($table, $function, $where=null, $args=null)
Definition: Db.php:626
getMigrate()
Definition: Db.php:786
$_migrate
Definition: Db.php:40
replace($table, $columns)
Replace a record (insert if not exists).
Definition: Db.php:538
lastInsertId()
Get the last auto-incremented ID.
Definition: Db.php:133
checkTimer($sql, $args)
Definition: Db.php:141
count($table, $where=null, $args=null)
The number of records that meet the requirements.
Definition: Db.php:639
logAddId($id)
Add a auto-increment ID to the latest change log.
Definition: Db.php:443
each($callback, $sql, $args=null)
Run a callback function for every row in an SQL resultset.
Definition: Db.php:307
$queryTime
Total time spent on queries (seconds).
Definition: Db.php:33
logChanges($table, $action, $columns, $where, $args=null)
Log the changes for an action.
Definition: Db.php:454
__call($func_name, $params)
Definition: Db.php:805
exists($table, $where=null, $args=null)
Check if a record exists that meets the requirement.
Definition: Db.php:620
config($key, $default=null)
Retrieve a config value.
Definition: Component.php:53
scout($table, $columns=null, $where=null, $args=null)
Pre-select records (will be used by select()).
Definition: Db.php:487
__invoke($sql, $args=null)
Definition: Db.php:828
version(&$hash=null)
Version without hash.
Definition: Fred.php:359
transaction($callback, $throw=true)
Wrap a callback function in a transaction.
Definition: Db.php:116
const ERROR
Error conditions.
Definition: Log.php:11
prepareArgs(&$sql, &$args)
Definition: Db.php:161
fetch($statement)
Fetch a row from an SQL statement.
Definition: Db.php:234
begin()
Begin a transaction.
Definition: Db.php:95
all($sql, $args=null)
Return al rows from an SQL statement.
Definition: Db.php:244
const ACTION_DELETE
Definition: Db.php:13
const CRITICAL
Critical conditions.
Definition: Log.php:10
$_version
Definition: Db.php:38
const ACTION_REPLACE
Definition: Db.php:11
date($time=null)
Convert a Unix timestamp to database date format.
Definition: Db.php:65
$migrateClassName
Class name for the migration tool.
Definition: Db.php:30
__sleep()
Definition: Db.php:832
$_attributes
Definition: Db.php:37
$_connection
Definition: Db.php:36
const ACTION_INSERT
Definition: Db.php:10
optimize($table=null)
Optimize table(s).
Definition: Db.php:741
rollBack()
Roll a transaction back.
Definition: Db.php:101
getVersion()
Definition: Db.php:799
prepareWhere(&$where, &$args=null, $glue='and')
Prepare a where statement.
Definition: Db.php:355
$logTables
Tables (key) and key columns (value) to log changes for.
Definition: Db.php:28
startTimer()
Definition: Db.php:137
$_startTime
Definition: Db.php:42
$defTables
Table definition to use (table name).
Definition: Db.php:29
sum($table, $column, $where=null, $args=null)
Total value from records that meet the requirements.
Definition: Db.php:683
isSelection($sql)
Check if an SQL statement is a select statement.
Definition: Db.php:81
$_logAddId
Definition: Db.php:44
const EVENT_LOG_ADD_ID
Definition: Db.php:17
$allowInsert
Allow inserts (for secondary processes like logs).
Definition: Db.php:34
const WARNING
Warning conditions.
Definition: Log.php:12
database()
Name of the current database.
Definition: Db.php:690
const EVENT_LOG_CHANGE
Definition: Db.php:16
query($sql, $args=null, $pdo=null)
Execute an SQL statement.
Definition: Db.php:220
multiColumns($columns)
Expand an array with scalar values and arrays to multiple arrays.
Definition: Db.php:326
$logTimes
Query time above which to add a note to the log (key = prio, value = edge; higher times first)...
Definition: Db.php:22
$_scout
Definition: Db.php:45
Basic component class.
Definition: Component.php:8
commit()
Commit a transaction.
Definition: Db.php:107
$_duploCount
Definition: Db.php:43
kill($id)
Kill a process.
Definition: Db.php:775
const EVENT_OPEN
Definition: Db.php:15
dateTime($time=null)
Convert a Unix timestamp to database date+time format.
Definition: Db.php:73
columns($table, $database=null)
Column properties for a table.
Definition: Db.php:706
update($table, $columns, $where=null, $args=null, $insert_if_not_exists=false)
Update one or more record(s).
Definition: Db.php:582
single($sql, $args=null, $auto=true)
Return a single row from an SQL statement.
Definition: Db.php:263
$logTimeSignificant
Number of significant figures (rounding).
Definition: Db.php:27
$statPrefix
Definition: Db.php:21
createPdo($attributes=null)
Definition: Db.php:47
getPdo()
Definition: Db.php:794
record($sql, $args=null)
Returns an array from an SQL statement.
Definition: Db.php:281
max($table, $column, $where=null, $args=null)
Biggest value from records that meet the requirements.
Definition: Db.php:661
logChange($table, $action, $key, $old, $new)
Log a change.
Definition: Db.php:436
$multiOperators
Definition: Db.php:19
$queryCount
Number of queries executed.
Definition: Db.php:32
keyColumn($table)
Primary key column.
Definition: Db.php:89
limit($sql, $limit, $offset=null)
Limit an SQL statement.
Definition: Db.php:425
execute($sql, $args=null, $pdo=null)
Execute an SQL statement.
Definition: Db.php:198
component($name)
Get a component (local or default).
Definition: Component.php:80
select($table, $columns=' *', $where=null, $args=null, $limit=null, $offset=null)
Select records.
Definition: Db.php:552
tables()
All tables in current database.
Definition: Db.php:697
statement($sql, $args=null, $pdo=null)
Create a PDO statement.
Definition: Db.php:180
$statTime
Operators (key) and glue (value) for multi column where conditions.
Definition: Db.php:20